[
  {
    "path": ".clang-format",
    "content": "# Generated from CLion C/C++ Code Style settings\nBasedOnStyle: LLVM\nAccessModifierOffset: -4\nAlignAfterOpenBracket: Align\nAlignConsecutiveAssignments: false\nAlignOperands: true\nAllowAllArgumentsOnNextLine: false\nAllowAllConstructorInitializersOnNextLine: false\nAllowAllParametersOfDeclarationOnNextLine: false\nAllowShortBlocksOnASingleLine: Always\nAllowShortCaseLabelsOnASingleLine: false\nAllowShortFunctionsOnASingleLine: All\nAllowShortIfStatementsOnASingleLine: Always\nAllowShortLambdasOnASingleLine: All\nAllowShortLoopsOnASingleLine: true\nAlwaysBreakAfterReturnType: None\nAlwaysBreakTemplateDeclarations: Yes\nBreakBeforeBraces: Custom\nBraceWrapping:\n  AfterCaseLabel: false\n  AfterClass: true\n  AfterControlStatement: Always\n  AfterEnum: true\n  AfterFunction: true\n  AfterNamespace: false\n  AfterUnion: true\n  BeforeCatch: false\n  BeforeElse: true\n  IndentBraces: false\n  SplitEmptyFunction: false\n  SplitEmptyRecord: true\nBreakBeforeBinaryOperators: None\nBreakBeforeTernaryOperators: true\nBreakConstructorInitializers: BeforeColon\nBreakInheritanceList: BeforeColon\nColumnLimit: 0\nCompactNamespaces: false\nContinuationIndentWidth: 8\nIndentCaseLabels: true\nIndentPPDirectives: None\nIndentWidth: 4\nKeepEmptyLinesAtTheStartOfBlocks: true\nMaxEmptyLinesToKeep: 2\nNamespaceIndentation: None\nObjCSpaceAfterProperty: false\nObjCSpaceBeforeProtocolList: true\nPointerAlignment: Left\nReflowComments: false\nSpaceAfterCStyleCast: true\nSpaceAfterLogicalNot: false\nSpaceAfterTemplateKeyword: false\nSpaceBeforeAssignmentOperators: true\nSpaceBeforeCpp11BracedList: false\nSpaceBeforeCtorInitializerColon: true\nSpaceBeforeInheritanceColon: true\nSpaceBeforeParens: ControlStatements\nSpaceBeforeRangeBasedForLoopColon: true\nSpaceInEmptyParentheses: false\nSpacesBeforeTrailingComments: 1\nSpacesInAngles: false\nSpacesInCStyleCastParentheses: false\nSpacesInContainerLiterals: false\nSpacesInParentheses: false\nSpacesInSquareBrackets: false\nTabWidth: 4\nUseTab: Never\nSortIncludes: Never\n"
  },
  {
    "path": ".gitignore",
    "content": "# Object files\n*.o\n*.ko\n*.obj\n*.elf\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Libraries\n*.lib\n*.a\n*.la\n*.lo\n\n# Shared objects (inc. Windows DLLs)\n*.dll\n*.so\n*.so.*\n*.dylib\n\n# Executables\n*.exe\n*.out\n*.app\n*.i*86\n*.x86_64\n*.hex\n\n# Debug files\n*.dSYM/\n\n# Hidden files\n.*\n!.clang-format\n!*/.gitkeep\n!.gitignore\n\n# Build and scratch directories\nscratch/\n*/scratch\nbuild*/\ngenerated/\ncmake-build-*\nthird-party/install/\nthird-party/rehost/\n\n# Windows folders\n*.ini\n\n# Python pyc\n*.pyc\n\n# CMake's compile_commnads used by clang-tools\ncompile_commands.json\n\n# Log files created by running sushi from somewhere in the repository\nlog.txt\n/vcpkg_installed/\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.28)\n\n# Require out-of-source builds\nfile(TO_CMAKE_PATH \"${PROJECT_BINARY_DIR}/CMakeLists.txt\" LOC_PATH)\nif(EXISTS \"${LOC_PATH}\")\n    message(FATAL_ERROR \"You cannot build in a source directory (or any directory with a CMakeLists.txt file). Please make a build subdirectory. Feel free to remove CMakeCache.txt and CMakeFiles.\")\nendif()\n\n#####################\n#  Project Version  #\n#####################\n\n# Don't change version anywhere else. Everything is generated from this.\nset(SUSHI_VERSION_MAJOR 1)\nset(SUSHI_VERSION_MINOR 3)\nset(SUSHI_VERSION_REVISION 0)\n\n# Versioning for the external api(s)\n# Update this to match the version above only if the external api (in include/sushi and/or rpc_interface/protos)\n# has been changed. Otherwise don't update.\nset(SUSHI_EXTERNAL_API_VERSION \"1.2.0\")\n\nproject(sushi_library\n    DESCRIPTION \"Headless plugin host for Elk Audio OS\"\n    HOMEPAGE_URL \"https://github.com/elk-audio/sushi\"\n    LANGUAGES CXX\n    VERSION ${SUSHI_VERSION_MAJOR}.${SUSHI_VERSION_MINOR}.${SUSHI_VERSION_REVISION}\n)\n\nif(NOT CMAKE_BUILD_TYPE)\n    set(CMAKE_BUILD_TYPE Debug)\nendif()\n\nset(CMAKE_MODULE_PATH \"${PROJECT_SOURCE_DIR}/cmake\" ${CMAKE_MODULE_PATH})\n\n##################################\n#  Generate build information    #\n##################################\n\n# Get the latest commit hash of the working branch\nexecute_process(\n    COMMAND git log -1 --format=%H\n    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}\n    OUTPUT_VARIABLE GIT_COMMIT_HASH\n    OUTPUT_STRIP_TRAILING_WHITESPACE\n)\n\nstring(TIMESTAMP BUILD_TIMESTAMP \"%Y-%m-%d %H:%M\")\n\nconfigure_file(\n    ${CMAKE_CURRENT_SOURCE_DIR}/include/version.h.in\n    ${CMAKE_BINARY_DIR}/generated/version.h\n)\n\n###########################\n#  Build and link options #\n###########################\n# The defaults enable all options and select APIs available for either Xenomai or macOS\nset(SUSHI_WITH_JACK_DEFAULT OFF)\nset(SUSHI_WITH_VST2_DEFAULT OFF)\nset(SUSHI_WITH_VST3_DEFAULT ON)\nset(SUSHI_LINK_VST3_DEFAULT ON)\nset(SUSHI_WITH_LINK_DEFAULT ON)\nset(SUSHI_WITH_RPC_INTERFACE_DEFAULT ON)\nset(SUSHI_TWINE_STATIC_DEFAULT OFF)\nset(SUSHI_AUDIO_BUFFER_SIZE_DEFAULT 64)\nset(SUSHI_WITH_SENTRY_DEFAULT OFF)\nset(SUSHI_SENTRY_DSN_DEFAULT \"--Sentry default DSN is undefined--\")\nset(SUSHI_DISABLE_MULTICORE_UNIT_TESTS_DEFAULT OFF)\nset(SUSHI_BUILD_STANDALONE_APP_DEFAULT ON)\nset(SUSHI_BUILD_WITH_SANITIZERS_DEFAULT OFF)\n\nset(SUSHI_RASPA_FLAVOR_DEFAULT \"evl\")\nset(TWINE_WITH_XENOMAI_DEFAULT OFF)\nset(TWINE_WITH_EVL_DEFAULT OFF)\n\nif (APPLE)\n    # macOS defaults\n    set(SUSHI_WITH_RASPA_DEFAULT OFF)\n    set(SUSHI_WITH_PORTAUDIO_DEFAULT OFF)\n    set(SUSHI_WITH_APPLE_COREAUDIO_DEFAULT ON)\n    set(SUSHI_WITH_ALSA_MIDI_DEFAULT OFF)\n    set(SUSHI_WITH_RT_MIDI_DEFAULT ON)\n    set(SUSHI_WITH_VST2_DEFAULT OFF)\n    set(SUSHI_WITH_LV2_DEFAULT ON)\n    set(SUSHI_WITH_LV2_MDA_TESTS_DEFAULT OFF)\n    set(SUSHI_WITH_UNIT_TESTS_DEFAULT ON)\n    set(SUSHI_BUILD_TWINE_DEFAULT ON)\n    set(SUSHI_DISABLE_MULTICORE_UNIT_TESTS_DEFAULT ON)\n\nelseif (MSVC)\n    # windows defaults\n    set(SUSHI_WITH_XENOMAI_DEFAULT OFF)\n    set(SUSHI_WITH_PORTAUDIO_DEFAULT ON)\n    set(SUSHI_WITH_ALSA_MIDI_DEFAULT OFF)\n    set(SUSHI_WITH_RT_MIDI_DEFAULT ON)\n    set(SUSHI_WITH_VST2_DEFAULT OFF)\n    set(SUSHI_WITH_LV2_DEFAULT OFF)\n    set(SUSHI_WITH_LV2_MDA_TESTS_DEFAULT OFF)\n    set(SUSHI_WITH_UNIT_TESTS_DEFAULT ON)\n    set(SUSHI_BUILD_TWINE_DEFAULT ON)\n\n    add_compile_definitions(_USE_MATH_DEFINES)\n\nelseif (CMAKE_CROSSCOMPILING)\n    # Yocto cross-compilation defaults\n    set(SUSHI_WITH_RASPA_DEFAULT ON)\n    set(SUSHI_WITH_PORTAUDIO_DEFAULT OFF)\n    set(SUSHI_WITH_APPLE_COREAUDIO_DEFAULT OFF)\n    set(SUSHI_WITH_ALSA_MIDI_DEFAULT ON)\n    set(SUSHI_WITH_RT_MIDI_DEFAULT OFF)\n    set(SUSHI_WITH_LV2_DEFAULT ON)\n    set(SUSHI_WITH_LV2_MDA_TESTS_DEFAULT OFF)\n    set(SUSHI_WITH_UNIT_TESTS_DEFAULT OFF)\n    set(SUSHI_BUILD_TWINE_DEFAULT OFF)\n\nelse()\n    # Native Linux defaults\n    set(SUSHI_WITH_JACK_DEFAULT ON)\n    set(SUSHI_WITH_RASPA_DEFAULT OFF)\n    set(SUSHI_WITH_PORTAUDIO_DEFAULT OFF)\n    set(SUSHI_WITH_APPLE_COREAUDIO_DEFAULT OFF)\n    set(SUSHI_WITH_ALSA_MIDI_DEFAULT ON)\n    set(SUSHI_WITH_RT_MIDI_DEFAULT OFF)\n    set(SUSHI_WITH_LV2_DEFAULT ON)\n    set(SUSHI_WITH_LV2_MDA_TESTS_DEFAULT ON)\n    set(SUSHI_WITH_UNIT_TESTS_DEFAULT ON)\n    set(SUSHI_BUILD_TWINE_DEFAULT ON)\nendif()\n\noption(SUSHI_WITH_RASPA \"Enable Raspa (xenomai) support\" ${SUSHI_WITH_RASPA_DEFAULT})\noption(SUSHI_WITH_JACK \"Enable Jack support\" ${SUSHI_WITH_JACK_DEFAULT})\noption(SUSHI_WITH_PORTAUDIO \"Enable PortAudio support\" ${SUSHI_WITH_PORTAUDIO_DEFAULT})\noption(SUSHI_WITH_APPLE_COREAUDIO \"Enable Apple CoreAudio support\" ${SUSHI_WITH_APPLE_COREAUDIO_DEFAULT})\noption(SUSHI_WITH_ALSA_MIDI \"Enable alsa midi support\" ${SUSHI_WITH_ALSA_MIDI_DEFAULT})\noption(SUSHI_WITH_RT_MIDI \"Enable RtMidi support\" ${SUSHI_WITH_RT_MIDI_DEFAULT})\noption(SUSHI_WITH_VST2 \"Enable Vst 2 support\" ${SUSHI_WITH_VST2_DEFAULT})\noption(SUSHI_WITH_VST3 \"Enable Vst 3 support\" ${SUSHI_WITH_VST3_DEFAULT})\noption(SUSHI_LINK_VST3 \"Link Vst 3 library to Sushi\" ${SUSHI_LINK_VST3_DEFAULT})\noption(SUSHI_WITH_LV2 \"Enable LV2 support\" ${SUSHI_WITH_LV2_DEFAULT})\noption(SUSHI_WITH_LV2_MDA_TESTS \"Include unit tests depending on LV2 drobilla MDA plugin port.\" ${SUSHI_WITH_LV2_MDA_TESTS_DEFAULT})\noption(SUSHI_WITH_UNIT_TESTS \"Build and run unit tests after compilation\" ${SUSHI_WITH_UNIT_TESTS_DEFAULT})\noption(SUSHI_WITH_LINK \"Enable Ableton Link support\" ${SUSHI_WITH_LINK_DEFAULT})\noption(SUSHI_WITH_RPC_INTERFACE \"Enable RPC control support\" ${SUSHI_WITH_RPC_INTERFACE_DEFAULT})\noption(SUSHI_BUILD_TWINE \"Build included Twine library\" ${SUSHI_BUILD_TWINE_DEFAULT})\noption(SUSHI_TWINE_STATIC \"Link against static version of TWINE library\" ${SUSHI_TWINE_STATIC_DEFAULT})\noption(SUSHI_WITH_SENTRY \"Enable sentry.io support\" ${SUSHI_WITH_SENTRY_DEFAULT})\noption(SUSHI_SENTRY_DSN \"Required Sentry DSN Path\" ${SUSHI_SENTRY_DSN_DEFAULT})\noption(SUSHI_DISABLE_MULTICORE_UNIT_TESTS \"Disable unit-tests dependent on multi-core processing.\" ${SUSHI_DISABLE_MULTICORE_UNIT_TESTS_DEFAULT})\noption(SUSHI_BUILD_STANDALONE_APP \"Build standalone Sushi executable\" ${SUSHI_BUILD_STANDALONE_APP_DEFAULT})\noption(SUSHI_BUILD_WITH_SANITIZERS \"Build Sushi with google address sanitizer on\" ${SUSHI_BUILD_WITH_SANITIZERS_DEFAULT})\noption(SUSHI_LINK_WITH_PIPEWIRE \"Link sushi with pipewire, only use if linking with a rasba build with Pipewire support\" OFF)\n\nset(SUSHI_AUDIO_BUFFER_SIZE ${SUSHI_AUDIO_BUFFER_SIZE_DEFAULT} CACHE STRING \"Set internal audio buffer size in frames\")\nset(SUSHI_RASPA_FLAVOR ${SUSHI_RASPA_FLAVOR_DEFAULT} CACHE STRING \"Set raspa flavor. Options are xenomai or evl\")\n\nif (SUSHI_WITH_ALSA_MIDI AND SUSHI_WITH_RT_MIDI)\n    message(FATAL_ERROR \"Both alsa midi and RtMidi set to on. Use only one midi frontend at a time\")\nendif()\n\nif (SUSHI_WITH_RASPA)\n    # Determine raspa evl/xenomai variables\n    if (${SUSHI_RASPA_FLAVOR} MATCHES \"evl\")\n        message(\"Building with Raspa/Twine EVL support\")\n        set(TWINE_WITH_EVL ON CACHE BOOL \"\" FORCE)\n    elseif (${SUSHI_RASPA_FLAVOR} MATCHES \"xenomai\")\n        message(\"Building with Raspa/Twine Xenomai support.\")\n        message(\"Warning Xenomai 3 support is deprecated and will be removed in future versions\")\n        set(TWINE_WITH_XENOMAI ON CACHE BOOL \"\" FORCE)\n    else ()\n        message(FATAL_ERROR \"Raspa flavor should be set to xenomai or evl\")\n    endif()\nendif()\n\n###############################\n# Sub-directory configuration #\n###############################\n\nif (${SUSHI_BUILD_TWINE})\n    set(TWINE_WITH_TESTS OFF CACHE BOOL \"\")\n    add_subdirectory(twine)\n\n    if (${SUSHI_TWINE_STATIC})\n        set(TWINE_LIB twine::twine_static)\n    else()\n        set(TWINE_LIB twine::twine)\n    endif()\n    set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} ${TWINE_LIB})\nelse()\n    find_package(twine 1.1)\n    set(INCLUDE_DIRS ${INCLUDE_DIRS} ${TWINE_PATH})\n    set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} ${TWINE_LIB})\n    find_package(elk-warning-suppressor 0.1.0 COMPONENTS warningSuppressor)\nendif()\n\nset(ELKLOG_WITH_UNIT_TESTS OFF CACHE BOOL \"\")\nset(ELKLOG_USE_INCLUDED_TWINE OFF CACHE BOOL \"\")\nset(ELKLOG_TWINE_STATIC ${SUSHI_TWINE_STATIC} CACHE STRING \"\")\nset(ELKLOG_WITH_EXAMPLES OFF CACHE BOOL \"\")\nset(ELKLOG_MULTITHREADED_RT_LOGGING OFF CACHE BOOL \"\")\nset(ELKLOG_FILE_SIZE 10000000 CACHE STRING \"\")\nset(ELKLOG_RT_MESSAGE_SIZE 2048 CACHE STRING \"\")\nset(ELKLOG_RT_QUEUE_SIZE 1024 CACHE STRING \"\")\n\nadd_subdirectory(elklog)\n\nadd_subdirectory(third-party EXCLUDE_FROM_ALL)\n\n###############\n# Raspa setup #\n###############\n\nif (${SUSHI_WITH_RASPA})\n    find_library(RASPA_LIB\n            NAMES raspa\n            PATHS /usr/lib /usr/local/lib\n            )\n    set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} ${RASPA_LIB} asound)\n    set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_RASPA)\n\n    if (${SUSHI_LINK_WITH_PIPEWIRE})\n        set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} pipewire-0.3)\n    endif()\n\n    if (NOT \"$ENV{CMAKE_SYSROOT}\" STREQUAL \"\")\n        set(CMAKE_SYSROOT \"$ENV{CMAKE_SYSROOT}\")\n        message(\"ENV_CMAKE_SYSROOT = \" $ENV{CMAKE_SYSROOT})\n    endif()\n\n    # Determine whether to link with evl or cobalt lib\n    if (${SUSHI_RASPA_FLAVOR} MATCHES \"xenomai\")\n\n        # External libraries:\n        # explicitly link with cobalt lib\n        set(XENOMAI_BASE_DIR \"/usr/xenomai\" CACHE STRING \"xenomai base dir path\")\n\n        if (NOT \"$ENV{CMAKE_SYSROOT}\" STREQUAL \"\")\n            set(CMAKE_SYSROOT \"$ENV{CMAKE_SYSROOT}\")\n            message(\"ENV_CMAKE_SYSROOT = \" $ENV{CMAKE_SYSROOT})\n        endif()\n\n        if (NOT \"${CMAKE_SYSROOT}\" STREQUAL \"\")\n            set(XENOMAI_BASE_DIR \"${CMAKE_SYSROOT}/usr/xenomai\")\n            message(\"XENOMAI_BASE_DIR is \" ${XENOMAI_BASE_DIR})\n        endif()\n\n        find_library(COBALT_LIB cobalt HINTS ${XENOMAI_BASE_DIR}/lib)\n        set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} ${COBALT_LIB})\n\n    elseif (${SUSHI_RASPA_FLAVOR} MATCHES \"evl\")\n        message(\"Building with EVL support\")\n        find_library(EVL_LIB\n            NAMES evl\n            PATHS /usr/lib /usr/local/lib\n            )\n        set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} ${EVL_LIB})\n    endif()\nendif()\n\n##############\n# Jack setup #\n##############\n\nif (${SUSHI_WITH_JACK})\n    message(\"Building with Jack support.\")\n\n    # Linked libraries\n    find_path(jack_dir NAMES \"jack/jack.h\")\n    find_library(jack_lib NAMES jack)\n    set(INCLUDE_DIRS ${INCLUDE_DIRS} ${jack_dir})\n    set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} ${jack_lib})\n\n    # Compile definitions\n    set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_JACK)\nendif()\n\n###################\n# PortAudio setup #\n###################\n\nif (${SUSHI_WITH_PORTAUDIO})\n    message(\"Building with PortAudio support.\")\n\n    set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} PortAudio)\n\n    # Compile definitions\n    set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_PORTAUDIO)\nendif()\n\nif (APPLE)\n    set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_APPLE_THREADING)\n\n    ###################\n    # CoreAudio setup #\n    ###################\n\n    if (${SUSHI_WITH_APPLE_COREAUDIO})\n        message(\"Building with Apple CoreAudio support.\")\n\n        # Compile definitions\n        set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_APPLE_COREAUDIO)\n\n        set(ADDITIONAL_APPLE_COREAUDIO_SOURCES  src/audio_frontends/apple_coreaudio/apple_coreaudio_utils.cpp\n                                                src/audio_frontends/apple_coreaudio/apple_coreaudio_object.cpp\n                                                src/audio_frontends/apple_coreaudio/apple_coreaudio_device.mm)\n\n        set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} \"-framework CoreAudio -framework Foundation\")\n    endif()\nendif()\n\n###################\n# Alsa midi setup #\n###################\n\nif (${SUSHI_WITH_ALSA_MIDI})\n    message(\"Building with alsa midi support.\")\n\n    # Additional sources\n    set(MIDI_SOURCES src/control_frontends/alsa_midi_frontend.h src/control_frontends/alsa_midi_frontend.cpp)\n\n    # linked libraries\n    find_path(ALSA_DIR NAMES \"alsa/asoundlib.h\")\n    find_library(ALSA_LIB NAMES asound)\n    set(INCLUDE_DIRS ${INCLUDE_DIRS} ${ALSA_DIR})\n    set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} ${ALSA_LIB})\n\n    # Compile definitions\n    set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_ALSA_MIDI)\nendif()\n\n################\n# RtMidi setup #\n################\n\nif (${SUSHI_WITH_RT_MIDI})\n    message(\"Building with RtMidi support.\")\n\n    # Additional sources\n    set(MIDI_SOURCES ${MIDI_SOURCES} src/control_frontends/rt_midi_frontend.cpp src/control_frontends/rt_midi_frontend.h)\n\n    #RtMidi library\n    find_package(RtMidi CONFIG REQUIRED)\n\n    set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} RtMidi::rtmidi)\n\n    set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_RT_MIDI)\nendif()\n\n###############\n# VST 2 setup #\n###############\n\nif (${SUSHI_WITH_VST2})\n    message(\"Building with Vst2 support.\")\n\n    set(ADDITIONAL_VST2_SOURCES src/library/vst2x/vst2x_wrapper.h\n                                src/library/vst2x/vst2x_host_callback.h\n                                src/library/vst2x/vst2x_plugin_loader.h\n                                src/library/vst2x/vst2x_midi_event_fifo.h\n                                src/library/vst2x/vst2x_processor_factory.h\n                                src/library/vst2x/vst2x_wrapper.cpp\n                                src/library/vst2x/vst2x_host_callback.cpp\n                                src/library/vst2x/vst2x_plugin_loader.cpp)\n\n    # Include dirs\n    if (VST_2_SDK_BASE_DIR)\n        set(SUSHI_VST2_SDK_PATH ${VST_2_SDK_BASE_DIR})\n    else()\n        find_file(SUSHI_VST2_SDK_PATH NAMES VST2_SDK vstsdk2.4 HINTS $ENV{HOME}/workspace\n                                                                     $ENV{HOME}/workspace/VST_SDK\n                                                                     $ENV{HOME}/workspace/vstsdk3610_11_06_2018_build_37)\n    endif()\n    if (${SUSHI_VST2_SDK_PATH} STREQUAL \"VST2_SDK_PATH-NOTFOUND\")\n        message(\"Warning! Could not find Steinberg SDK for VST 2.4\")\n    endif()\n\n    set(INCLUDE_DIRS ${INCLUDE_DIRS} ${SUSHI_VST2_SDK_PATH}/pluginterfaces/vst2.x)\n\n    # Compile definitions\n    set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_VST2 -D__cdecl=)\nendif()\n\n###############\n# VST 3 setup #\n###############\n\n# TODO: use this ${SUSHI_LINK_VST3}, and when it's off, link to VST3 SDK included by JUCE.\nif (${SUSHI_WITH_VST3})\n    message(\"Building with Vst3 support.\")\n\n    # Vst 3 host:\n    # Build the given Vst3 host implementation into a separate library to avoid\n    # Vst specific defines leaking into our code.\n\n    set(VST3_SDK_PATH \"${PROJECT_SOURCE_DIR}/third-party/vst3sdk\")\n    set(VST3_HOST_SOURCES\n            \"${VST3_SDK_PATH}/public.sdk/source/vst/hosting/eventlist.cpp\"\n            \"${VST3_SDK_PATH}/public.sdk/source/vst/hosting/parameterchanges.cpp\"\n            \"${VST3_SDK_PATH}/public.sdk/source/vst/hosting/hostclasses.cpp\"\n            \"${VST3_SDK_PATH}/public.sdk/source/vst/hosting/module.cpp\"\n            \"${VST3_SDK_PATH}/public.sdk/source/vst/utility/stringconvert.cpp\"\n            \"${VST3_SDK_PATH}/public.sdk/source/vst/hosting/pluginterfacesupport.cpp\"\n            \"${VST3_SDK_PATH}/public.sdk/source/common/memorystream.cpp\")\n\n    if (MSVC)\n        set(VST3_HOST_SOURCES ${VST3_HOST_SOURCES} \"${VST3_SDK_PATH}/public.sdk/source/vst/hosting/module_win32.cpp\")\n    endif()\n\n    if(NOT MSVC)\n        set(VST3_COMPILE_FLAGS \"-Wno-unused-parameter \\\n                                -Wno-extra \\\n                                -Wno-deprecated \\\n                                -Wno-cpp \\\n                                -Wno-pointer-bool-conversion \\\n                                -Wno-suggest-override \\\n                                -Wno-deprecated-declarations \\\n                                -Wno-shorten-64-to-32 \\\n                                -Wformat=0\")\n    endif()\n\n    if (${CMAKE_SYSTEM_NAME} STREQUAL \"Linux\")\n        set(VST3_HOST_SOURCES ${VST3_HOST_SOURCES} \"${VST3_SDK_PATH}/public.sdk/source/vst/hosting/module_linux.cpp\")\n        set(AUDIOHOST_PLATFORM_LIBS)\n    endif()\n\n    if (${CMAKE_SYSTEM_NAME} STREQUAL \"Darwin\")\n        set(VST3_HOST_SOURCES\n            ${VST3_HOST_SOURCES}\n            \"${VST3_SDK_PATH}/public.sdk/source/vst/hosting/module_mac.mm\")\n        set(AUDIOHOST_PLATFORM_LIBS \"-framework Cocoa\")\n        set(VST3_COMPILE_FLAGS \"${VST3_COMPILE_FLAGS} -fobjc-arc -Wno-unqualified-std-cast-call\")\n    endif()\n\n    add_library(vst3_host STATIC ${VST3_HOST_SOURCES})\n    target_link_libraries(vst3_host PRIVATE ${AUDIOHOST_PLATFORM_LIBS} elk_vst3_extensions)\n    set_target_properties(vst3_host PROPERTIES EXCLUDE_FROM_ALL true)\n    target_compile_features(vst3_host PUBLIC cxx_std_20)\n    if (\"${CMAKE_BUILD_TYPE}\" STREQUAL \"Release\")\n        set_target_properties(vst3_host PROPERTIES COMPILE_FLAGS \"-DRELEASE ${VST3_COMPILE_FLAGS}\")\n    else()\n        set_target_properties(vst3_host PROPERTIES COMPILE_FLAGS \"-DDEVELOPMENT ${VST3_COMPILE_FLAGS}\")\n    endif()\n\n    target_include_directories(vst3_host PRIVATE \"${PROJECT_SOURCE_DIR}/third-party/vst3sdk\")\n    add_dependencies(vst3_host sdk)\n\n    # Set sources\n    set(ADDITIONAL_VST3_SOURCES src/library/vst3x/vst3x_wrapper.h\n                                src/library/vst3x/vst3x_host_app.h\n                                src/library/vst3x/vst3x_utils.h\n                                src/library/vst3x/vst3x_processor_factory.h\n                                src/library/vst3x/vst3x_wrapper.cpp\n                                src/library/vst3x/vst3x_host_app.cpp\n                                src/library/vst3x/vst3x_utils.cpp\n                                src/library/vst3x/vst3x_file_utils.cpp)\n\n    # Linked libraries\n    set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} vst3_host sdk elk_vst3_extensions)\n\n    # Compiler definitions\n    set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_VST3)\n\n\n    # In yocto env / cross compiling scenarios, this is not needed as mda-vst3 plugins are\n    # built already. Also, this causes an \"exec-format\" error since the plugins need a specific\n    # method of compilation during cross compilation process.\n    if (NOT CMAKE_CROSSCOMPILING)\n        # Triggering mda-vst3 build - used by sample sushi json configurations under misc/config_files.\n        set_target_properties(mda-vst3 PROPERTIES EXCLUDE_FROM_ALL False)\n    endif()\nendif()\n\n#############\n# LV2 setup #\n#############\n\nif (${SUSHI_WITH_LV2})\n    message(\"Building with LV2 support.\")\n\n    find_package(lv2 CONFIG REQUIRED)\n    find_package(PkgConfig REQUIRED)\n    pkg_check_modules(LILV REQUIRED IMPORTED_TARGET lilv-0)\n\n    set(ADDITIONAL_LV2_SOURCES src/library/lv2/lv2_wrapper.h\n                               src/library/lv2/lv2_control.h\n                               src/library/lv2/lv2_features.h\n                               src/library/lv2/lv2_host_nodes.h\n                               src/library/lv2/lv2_model.h\n                               src/library/lv2/lv2_port.h\n                               src/library/lv2/lv2_state.h\n                               src/library/lv2/lv2_processor_factory.h\n                               src/library/lv2/lv2_wrapper.cpp\n                               src/library/lv2/lv2_state.cpp\n                               src/library/lv2/lv2_worker.cpp\n                               src/library/lv2/lv2_features.cpp\n                               src/library/lv2/lv2_model.cpp\n                               src/library/lv2/lv2_port.cpp\n                               src/library/lv2/lv2_control.cpp)\n\n    # Linked libraries\n    set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} PkgConfig::LILV lv2_host)\n\n    # Compiler definitions\n    set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_LV2)\nendif()\n\n##############\n# gRPC setup #\n##############\n\nif (${SUSHI_WITH_RPC_INTERFACE})\n    message(\"Building with RPC support.\")\n\n    add_subdirectory(rpc_interface)\n\n    # Linked libraries\n    set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} sushi_rpc)\n\n    # Compiler definitions\n    set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_RPC_INTERFACE)\nendif()\n\n######################\n# Ableton link setup #\n######################\n\nif (${SUSHI_WITH_LINK})\n    message(\"Building with Ableton Link support.\")\n\n    include(third-party/link/AbletonLinkConfig.cmake)\n\n    # Linked libraries\n    set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} Ableton::Link)\n\n    # Compiler definitions\n    set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_ABLETON_LINK)\nendif()\n\nmessage(\"Configured audio buffer size: \" ${SUSHI_AUDIO_BUFFER_SIZE} \" samples\")\n\n###################\n# sentry.io setup #\n###################\n\nif (${SUSHI_WITH_SENTRY})\n    find_package(Threads)\n    find_package(ZLIB)\n    find_package(sentry CONFIG REQUIRED)\n\n    message(\"Building with sentry.io support.\")\n\n    if (NOT SUSHI_SENTRY_DSN)\n        set(SUSHI_SENTRY_DSN ${SUSHI_SENTRY_DSN_DEFAULT} CACHE STRING \"The DSN to upload the sentry logs and crash reports to, from Sushi\" FORCE)\n    endif()\n\n    set(SENTRY_CRASHPAD_HANDLER_PATH \"${VCPKG_INSTALLED_DIR}/osx-custom/tools/sentry-native/crashpad_handler\")\n\n    set(EXTRA_BUILD_LIBRARIES ${EXTRA_BUILD_LIBRARIES} sentry::sentry)\n    set(EXTRA_COMPILE_DEFINITIONS ${EXTRA_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_SENTRY -DSUSHI_SENTRY_DSN=\"${SUSHI_SENTRY_DSN}\")\nendif()\n\n\n####################\n#  Main Target     #\n####################\n\nset(SOURCE_FILES\n    src/utils.cpp\n    src/audio_frontends/base_audio_frontend.cpp\n    src/concrete_sushi.cpp\n    src/audio_frontends/offline_frontend.cpp\n    src/audio_frontends/reactive_frontend.cpp\n    src/audio_frontends/jack_frontend.cpp\n    src/audio_frontends/portaudio_frontend.cpp\n    src/audio_frontends/apple_coreaudio_frontend.cpp\n    src/audio_frontends/portaudio_devices_dump.cpp\n    src/audio_frontends/coreaudio_devices_dump.cpp\n    src/audio_frontends/xenomai_raspa_frontend.cpp\n    src/audio_frontends/offline_frontend.cpp\n    src/audio_frontends/reactive_frontend.cpp\n    src/control_frontends/base_control_frontend.cpp\n    src/control_frontends/oscpack_osc_messenger.cpp\n    src/control_frontends/reactive_midi_frontend.cpp\n    src/control_frontends/osc_frontend.cpp\n    src/dsp_library/biquad_filter.cpp\n    src/engine/audio_engine.cpp\n    src/engine/audio_graph.cpp\n    src/engine/event_dispatcher.cpp\n    src/engine/track.cpp\n    src/engine/midi_dispatcher.cpp\n    src/engine/json_configurator.cpp\n    src/engine/receiver.cpp\n    src/engine/event_timer.cpp\n    src/engine/transport.cpp\n    src/engine/parameter_manager.cpp\n    src/engine/processor_container.cpp\n    src/engine/plugin_library.cpp\n    src/engine/controller/controller.cpp\n    src/engine/controller/system_controller.cpp\n    src/engine/controller/transport_controller.cpp\n    src/engine/controller/timing_controller.cpp\n    src/engine/controller/keyboard_controller.cpp\n    src/engine/controller/audio_graph_controller.cpp\n    src/engine/controller/parameter_controller.cpp\n    src/engine/controller/program_controller.cpp\n    src/engine/controller/midi_controller.cpp\n    src/engine/controller/audio_routing_controller.cpp\n    src/engine/controller/cv_gate_controller.cpp\n    src/engine/controller/osc_controller.cpp\n    src/engine/controller/session_controller.cpp\n    src/engine/controller/real_time_controller.cpp\n    src/factories/base_factory.cpp\n    src/factories/reactive_factory.cpp\n    src/factories/reactive_factory_implementation.cpp\n    src/factories/standalone_factory.cpp\n    src/factories/standalone_factory_implementation.cpp\n    src/factories/offline_factory.cpp\n    src/factories/offline_factory_implementation.cpp\n    src/library/event.cpp\n    src/library/midi_decoder.cpp\n    src/library/midi_encoder.cpp\n    src/library/internal_plugin.cpp\n    src/library/performance_timer.cpp\n    src/library/parameter_dump.cpp\n    src/library/processor.cpp\n    src/library/processor_state.cpp\n    src/library/plugin_registry.cpp\n    src/library/internal_processor_factory.cpp\n    src/library/lv2/lv2_processor_factory.cpp\n    src/library/vst2x/vst2x_processor_factory.cpp\n    src/library/vst3x/vst3x_processor_factory.cpp\n    src/plugins/arpeggiator_plugin.cpp\n    src/plugins/control_to_cv_plugin.cpp\n    src/plugins/cv_to_control_plugin.cpp\n    src/plugins/gain_plugin.cpp\n    src/plugins/lfo_plugin.cpp\n    src/plugins/passthrough_plugin.cpp\n    src/plugins/equalizer_plugin.cpp\n    src/plugins/freeverb_plugin.cpp\n    src/plugins/peak_meter_plugin.cpp\n    src/plugins/return_plugin.cpp\n    src/plugins/sample_player_plugin.cpp\n    src/plugins/sample_player_voice.cpp\n    src/plugins/sample_delay_plugin.cpp\n    src/plugins/send_plugin.cpp\n    src/plugins/send_return_factory.cpp\n    src/plugins/step_sequencer_plugin.cpp\n    src/plugins/transposer_plugin.cpp\n    src/plugins/wav_streamer_plugin.cpp\n    src/plugins/wav_writer_plugin.cpp\n    src/plugins/mono_summing_plugin.cpp\n    src/plugins/stereo_mixer_plugin.cpp\n    src/plugins/brickworks/compressor_plugin.cpp\n    src/plugins/brickworks/cab_sim_plugin.cpp\n    src/plugins/brickworks/ring_mod_plugin.cpp\n    src/plugins/brickworks/bitcrusher_plugin.cpp\n    src/plugins/brickworks/wah_plugin.cpp\n    src/plugins/brickworks/eq3band_plugin.cpp\n    src/plugins/brickworks/phaser_plugin.cpp\n    src/plugins/brickworks/chorus_plugin.cpp\n    src/plugins/brickworks/vibrato_plugin.cpp\n    src/plugins/brickworks/flanger_plugin.cpp\n    src/plugins/brickworks/combdelay_plugin.cpp\n    src/plugins/brickworks/saturation_plugin.cpp\n    src/plugins/brickworks/noise_gate_plugin.cpp\n    src/plugins/brickworks/tremolo_plugin.cpp\n    src/plugins/brickworks/notch_plugin.cpp\n    src/plugins/brickworks/multi_filter_plugin.cpp\n    src/plugins/brickworks/highpass_plugin.cpp\n    src/plugins/brickworks/clip_plugin.cpp\n    src/plugins/brickworks/fuzz_plugin.cpp\n    src/plugins/brickworks/dist_plugin.cpp\n    src/plugins/brickworks/drive_plugin.cpp\n    src/plugins/brickworks/simple_synth_plugin.cpp\n)\n\nadd_library(${PROJECT_NAME} STATIC \"${SOURCE_FILES}\"\n                                   \"${ADDITIONAL_APPLE_COREAUDIO_SOURCES}\"\n                                   \"${ADDITIONAL_VST2_SOURCES}\"\n                                   \"${ADDITIONAL_VST3_SOURCES}\"\n                                   \"${ADDITIONAL_LV2_SOURCES}\"\n                                   \"${MIDI_SOURCES}\")\n\nset_target_properties(${PROJECT_NAME} PROPERTIES LINKER_LANGUAGE CXX)\n\n#########################\n#  Include Directories  #\n#########################\n\nset(INCLUDE_DIRS ${INCLUDE_DIRS}\n                 \"${PROJECT_SOURCE_DIR}/src\"\n                 \"${PROJECT_SOURCE_DIR}\"\n                 \"${PROJECT_SOURCE_DIR}/third-party/vst3sdk\"\n                 \"${PROJECT_SOURCE_DIR}/third-party/link/include\"\n                 \"${PROJECT_SOURCE_DIR}/third-party/brickworks/include\"\n)\n\n\nset(PUBLIC_INCLUDE_DIRS \"${CMAKE_BINARY_DIR}\" # for generated version.h\n                        \"${PROJECT_SOURCE_DIR}/third-party/optionparser/\"\n                        \"${PROJECT_SOURCE_DIR}/third-party/rapidjson/include\")\n\n#################################\n#  Linked libraries             #\n#################################\n\nfind_package(SndFile CONFIG REQUIRED)\n\nset(COMMON_LIBRARIES\n    SndFile::sndfile\n    fifo\n    oscpack\n    elklog\n    freeverb\n    elk::warningSuppressor\n    ${TWINE_LIB}\n)\n\nif(NOT MSVC)\n    set(COMMON_LIBRARIES ${COMMON_LIBRARIES} pthread dl)\n\n    set_target_properties(oscpack PROPERTIES COMPILE_FLAGS \"-Wno-deprecated-declarations\")\nendif()\n\nif (${CMAKE_SYSTEM_NAME} STREQUAL \"Linux\" AND ${CMAKE_CXX_COMPILER_ID} STREQUAL \"Clang\")\n    set(COMMON_LIBRARIES ${COMMON_LIBRARIES} atomic)\nendif()\n\ntarget_include_directories(${PROJECT_NAME}\n        PRIVATE\n            ${INCLUDE_DIRS}\n        PUBLIC\n            ${PUBLIC_INCLUDE_DIRS}\n            # where top-level project will look for the library's public headers\n            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>\n)\n\ntarget_link_libraries(${PROJECT_NAME} PUBLIC ${EXTRA_BUILD_LIBRARIES} ${COMMON_LIBRARIES})\n\n####################################\n#  Compiler Flags and definitions  #\n####################################\n\nif(MSVC)\n    # C5045: Compiler will insert Spectre mitigation for memory load if /Qspectre switch specified\n    #        is disabled for all - Spectre is not a concern for Sushi I don't think.\n\n    # C4996: Deprecated warning. This was flooding the terminal,\n    #        I might remove the suppression from here eventually, and out into the code again.\n    target_compile_options(${PROJECT_NAME} PRIVATE /wd4996 /wd5045)\nelse ()\n    target_compile_options(${PROJECT_NAME} PUBLIC -Wall -Wextra -Wno-psabi -fPIC -ffast-math)\n\n    target_compile_options(${PROJECT_NAME} PRIVATE -fno-rtti)\n\n    if (SUSHI_BUILD_WITH_SANITIZERS)\n        target_compile_options(${PROJECT_NAME}  PUBLIC -fsanitize=address -g)\n    endif()\nendif()\n\ntarget_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20)\ntarget_compile_definitions(${PROJECT_NAME} PUBLIC -DSUSHI_CUSTOM_AUDIO_CHUNK_SIZE=${SUSHI_AUDIO_BUFFER_SIZE} ${EXTRA_COMPILE_DEFINITIONS})\n\n#####################\n#  Sub projects     #\n#####################\n\nif (${SUSHI_BUILD_STANDALONE_APP})\n    add_subdirectory(apps)\nendif()\n\n######################\n#  Tests subproject  #\n######################\n\nif (${SUSHI_WITH_UNIT_TESTS})\n    add_subdirectory(test)\nendif()\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Elk Audio OS\n\n## Github Issues\n\nYou can open a Github issue to signal a bug or request a new feature. Please don't create an issue to ask about general help, use the [Elk Audio Forum](https://forum.elk.audio) for generic questions and assistance.\n\nWhen writing a bug report, try to be as complete as you can regarding the context:\n  * Mention the version of Elk Audio OS that you are testing\n  * What is your run-time setup (plugins, buffer size, audio/CV channels, sensors, etc.)\n  * (if working on a plugin) what is your build environment\n  * If the issue is about SUSHI, TWINE or a plugin, try to reproduce it with the SUSHI AppImage for Linux or using another audio frontend (offline, dummy)\n  * Include relevant files that helps understand your issue or, better, to reproduce it\n\n## Submitting a PR\n\nA signed Contributor Agreement (CA) is required for submitting Pull Requests to this repository. Please contact tech@elk.audio for more information.\n\n"
  },
  {
    "path": "COPYING",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "HISTORY.md",
    "content": "## 1.3.0\nNew Features:\n  * Tracks can be explicitly allocated to threads\n  * Send and return plugins are now zero delay under some conditions\n  * Updated vcpkg\n  * Send and return plugins are now zero-delay if on the same cpu core\n  * Vst3 extension apis\n  * Updated gRPC api with versioning and command completion notifications\n  * Updated example config files to use initial state instead of events\n  * String properties can now be output only, i.e. not settable by Sushi\n\nFixes:\n  * Warning if multiple twine workers are allocated to the same cpu core\n  * Fixes for CMake 4\n  * Updates for deprecated features in C++20\n  * Refactored processor channel config to be less stateful\n  * Fixed issue with VST2 plugins with > 16 channels\n  * Vst3 non-automatable parameter bug fixed\n  * Vst3 32 bit crash on load fixed\n\n## 1.2.0\n\nNew Features:\n  * Support for using Sushi as a library\n  * Windows support\n  * C++20 support\n  * Updated Elklog\n  * Updated Twine to 1.0\n  * Warning suppressor library included\n\nFixes:\n  * Improved handling of audio pause / resume\n  * Xrun detection in audio frontends\n  * Improved handling of processor add/delete if audio thread is not running\n  * Better event lifetime management\n  * EventDispatcher thread could stall if system clock changed\n  * Fix for Vst2 plugins with > 16 channels\n  * All example config files now uses \"initial_state\" to set initial parameter values\n\n## 1.1.0\n\nNew features:\n  * Added Apple CoreAudio frontend\n  * Support for aggregate devices\n  * Brickworks plugin suite added\n  * Wave Streamer plugin\n  * Freeverb plugin\n  * TWINE updated to 0.4.0 with workgroup and EVL support\n  * Rapidjson updated to latest master branch\n  * Optimised State changes for VST3 plugins\n  * Crash logging with Sentry\n  * Device dump now includes sample rate and buffer size support\n\nFixes:\n  * LV2 worker request space too small\n  * LV2 logging not handled correctly\n  * Incorrect velocity scaling on Midi output from LV2 plugins\n  * VST3 plugin initialization order fix\n  * Parameter notification fix\n  * Incorrect file length display in WaveStreamer plugin\n  * Seek precision fix in WaveStreamer plugin\n  * Audio channel setup now only called when plugin is disabled\n  * Send and return crash at exit\n\nBreaking changes:\n  * The argument --dump-portaudio-devs is changed to --dump-audio-devices and now requires a frontend to be specified (--coreaudio or --portaudio).\n  * Failure to set priority and/or workgroup on macOS will cause Sushi to exit.\n\n## 1.0.0\n\nNew features:\n  * clang 13+ support\n  * gcc 10+ support\n  * macOS support\n  * VST2 & VST3 bundle support for macOS\n  * Portaudio frontend\n  * RtMIDI frontend\n  * MIDI clock output\n  * Processor state save & reload\n  * Plugin binary state setting\n  * Session state save & reload\n  * Refactor track modes w.r.t. channel configuration\n  * \"pre\" and \"post\" tracks to process engine channels before/after dispatching them to tracks\n  * Audio frontends pause/restart\n  * Switch from liblo to oscpack for OSC\n  * Option to set target OSC IP\n  * Channel specification in send&return internal plugins\n  * Extended parameter notifications, now multiple controllers can be updated easily at the same time\n  * LV2 parameter output support\n  * Command-line switches to disable OSC and gRPC\n  * Use vcpkg to manage most of third-party dependencies\n  * Refactored CMake configuration, now using SUSHI_ for options prefix\n  * Ableton Link updated to v3.0.5\n  * TWINE updated to v0.3.2\n  * VST3 SDK updated to v3.7.6\n\nFixes:\n  * Workaround for delayed ALSA MIDI event output\n  * Crash in MIDI dispatcher when deleting track connections\n  * Various JSON parsing fixes\n  * Memory leak in VST3 wrapper\n  * Issue with setting different samplerate from JSON and audio frontend\n  * Slow response to large numbers of VST3 parameter changes\n  * Peakmeter formatted level parameter scaling\n\nBreaking changes:\n  * All build options renamed to SUSHI_XXX_YYY from XXX_YYY\n  * Vcpgk initalization step required at first build. See README.md\n  * Track channel config has been simplified. The json \"mode\" member has been replaced with an integer \"channels\" member. To create a multibus track, an additional member \"multibus\" must be set to true and the number of buses specified with the \"buses\" member.\n  * Tracks now only report number of channels (and buses in the multibus case) and input and output channels and busses are assumed to be the same.\n  * All instances of \"busses\" in json config and grpc api has been corrected to \"buses\"\n\n## 0.12.0\n\nNew Features:\n  * Internal send and return plugins\n  * Stereo mixer internal plugin\n  * Sample delay internal plugin\n  * Multi channel support in peak meter plugin\n  * Timing and transport notifications\n  * Processor properties support\n  * Master limiter\n  * Updated VST SDK to v3.7.3\n  * Updated Twine to v0.2.1\n  * Build support for GCC10\n\nFixes:\n  * gRPC segfault on exit\n  * Refactored plugin instantiation architecture\n  * Track noise for tracks with no audio input\n  * Bug where events outputted before the audio process callback were lost\n  * Race condition in OSC path registration\n  * LV2 plugin load crash\n\n## 0.11.0\n\nNew Features:\n  * Expanded gRPC control interface, including push-style notifications\n  * Bumped recommended gRPC version\n  * Dynamic loading and routing of tracks and plugins\n  * Configurable OSC parameter output\n  * Wav writer plugin\n  * Mono summing plugin\n  * Improved peak meter plugin\n\nFixes:\n  * Aftertouch messages not forwarded in Alsa midi frontend\n  * Ensuring silence when track gain is 0\n  * Fix for generate script when python2.7 is missing\n  * Fix to accommodate v1.16 and v1.24 of gRPC libraries\n  * Raspa frontend initialisation order fix\n  * Internal event system refactor\n  * Controller class refactor and split into sub-controllers\n  * Logging library built statically - faster compile time\n  * Fix for timing sensitive unit tests\n  * Audio routing bug for mono tracks\n\n## 0.10.3\n\nFixes:\n  * Mono tracks can now be connected to stereo output busses\n  * OSC paths for gain and pan controls on tracks work again\n\n## 0.10.2\n\nFixes:\n  * LV2 parameter handling fix for non-sequential parameter ids\n\n## 0.10.1\n\nNew Features:\n  * LV2 worker thread extension support\n  * Parameter values are now always normalised\n\nFixes:\n  * LV2 string parameter value fix\n\n## 0.10.0\n\nNew Features:\n  * LV2 plugin support\n  * Ableton Link support\n  * Multiple midi input and output ports\n  * Extended OSC control API making it more similar to gRPC\n\nFixes:\n  * Updated example configuration files\n\n## 0.9.1\n\nNew Features:\n  * Plugin information dump with CL switch\n  * Updated VST 3 SDK to latest 3.14\n\nFixes:\n  * Allows plugin parameters with duplicate names\n  * Events section not required anymore with any frontend\n\n## 0.9.0\n\nNew Features:\n  * CV and Gate support + related example plugins\n  * Step sequencer example plugin\n  * gRPC listening address configurable through command line\n\nFixes:\n  * Parameter Units now passed correctly\n  * Faster Json file loading\n  * Better Json parsing error printing\n  * Removed Raspalib submodule\n  * Unit test fixes\n\n## 0.8.0\n\nNew Features:\n  * gRPC control API\n  * Automatic clip detection on master tracks\n  * Smooth track parameter controls at audio rate\n  * Simple MIDI transposer internal plugin\n  * Support relative mode for MIDI CCs mapping\n  * (x86 / XMOS) : support in RASPA for USB Midi gadget\n  * VST 2.x support is now an optional build feature\n  * Support VST 3 programs\n  * Added an option to automatically flush the logs at a periodic interval\n\nFixes:\n  * TWINE: Automatic denormal handling in all threads\n  * Raspa: fix for affinity of non-RT threads after initialization\n  * Raspa on x86/XMOS: fixes for spurious startup synchronization\n  * MIDI: explicitly convert NoteON with zero velocity to NoteOFF messages\n  * Various VST 3 fixes\n  * Log file can now be specified at any location\n  * VST 2.x : allow host callback functions to be called during plugin initialization\n\n## 0.7.0\n\n  * Multicore track rendering through Twine library\n  * Processor performance timings statistics\n  * Dummy frontend for investigating RT safety issues in plugins\n\n## 0.6.2\n\n  * Fix RASPA plugin initialization issue\n  * Fix parameter output from VST 2.x plugins\n\n## 0.6.1\n\n  * Fix: handling of track pan for multichannel plugins\n  * Fix: erroneous channel handling for offline frontend\n\n## 0.6\n\n  * Multichannel track/buses architecture and support for VST multichannel plugins\n  * Handling of events and MIDI outputs from plugins\n  * Time information handling\n  * Scheduling of asynchronous tasks from internal plugins\n  * Arpeggiator and peak meter internal plugins\n  * Many fixes and improvements to RASPA frontend and VST wrappers\n\n## 0.5.1\n\n  * RASPA Xenomai frontend\n  * Multichannel I/O from audio frontends\n\n## 0.5\n\n  * VST 3.6 wrapper\n  * Dynamic plugin loading\n\n## 0.4\n\n  * VST 2.4 wrapper\n  * Xenomai offline frontend\n  * More advanced MIDI routing and mapping\n\n## 0.3\n\n  * JACK audio frontend\n  * MIDI support\n  * OSC control\n\n## 0.2\n\n  * Events handling\n  * Internal plugins (gain, eq, sampler)\n\n## 0.1\n\n  * Initial version\n  * Offline frontend with I/O with libsndfile\n"
  },
  {
    "path": "LICENSE.md",
    "content": "#Licensing information\n\nAll SUSHI code is Copyright © 2017-2023 [Elk Audio AB], formerly [Modern Ancient Instruments Networked AB] (“Elk”) and is licensed under the [https://www.gnu.org/licenses/agpl-3.0.en.html](GNU Affero General Public License v.3) (“AGPLv3”). Such SUSHI code is considered the “Program” within the meaning of the AGPLv3.\n\nLoading plugins or libraries into the Program, or statically or dynamically linking or interfacing other applications, plugins, or libraries with the Program, including through a network API, is making a combined work based on the Program. Thus, the AGPLv3 covers the entire combination. You may therefore propagate, convey, or make available over a network, this combined work only in full compliance with the AGPLv3, including compliance with the obligation to provide the Corresponding Source (as that term is defined and used in the AGPLv3) for this entire combination.\n\nAs a special exception, Elk gives you permission to combine the Program with plugins, libraries or applications that are either:\n\n * made available by you under the GNU General Public License v2 or v3, or the GNU Lesser General Public License v2 or v3; or\n\n * made available by you without charge or royalty under other open source or proprietary terms, regardless of whether such terms conflict or are incompatible with the AGPLv3, but only if you make available to any requesting party the Corresponding Source of such plugins, libraries, or applications without charge under license terms that permit commercial royalty-free use, modification and distribution.\n\nThis exception is an additional permission under section 7 of the AGPLv3.\n\n#Additional Notes\n\nThe Elk name, design, and logo are trademarks or registered trademarks of Elk Audio AB. No permission to use is granted, except that you may use “[for Elk Audio OS]” for promotion of your plugin and “[Elk Powered]” for redistributions of modified versions. For any other uses, please contact  info@elk.audio."
  },
  {
    "path": "README.md",
    "content": "# SUSHI\nHeadless plugin host for ELK Audio OS.\n\n## Sushi as Library\nSushi can be used as a standalone terminal application, and can now also be used as a library.\n\nFor more information on the latter, refer to the [LIBRARY.md] (docs/LIBRARY.md) file in this repository.\n\n## Usage\n\nThe Sushi standalone project, and consequently the produced binary, reside in the `./apps/` sub-folder of this repository.\n\nSee `sushi -h` for a complete list of options.\nCommon use cases are:\n\nTest in offline mode with I/O from audio file:\n\n    $ sushi -o -i input_file.wav -c config_file.json\n\nUse Core Audio on macOS for realtime audio, with the default devices:\n\n    $ sushi --coreaudio -c config_file.json\n\nUse JACK for realtime audio:\n\n    $ sushi -j -c config_file.json\n\nWith JACK, Sushi creates 8 virtual input and output ports that you can connect to other programs or system outputs.\n\n## Sushi macOS\nSince version 1.0, Sushi can be built natively for macOS as a native binary with all the dependencies statically linked to it.\n\nThere is a new Core Audio frontend (selectable with the `--coreaudio` command-line option) to interface directly with Core Audio. As an alternative, a Portaudio frontend is also available (with the `--portaudio` flag).\n\nWith Core Audio, you can select other devices than the default with the `--audio-input-device-uid` and `--audio-output-device-uid` options. To find out the right number there, you can launch Sushi with the `--dump-audio-devices` together with `--coreaudio` to get a list in JSON format printed to stdout.\n\nMIDI support is provided through RtMidi and can access CoreMidi devices directly.\n\nLV2 support is currently not available for macOS.\n\n## Sushi Windows\nSince version 1.2, Sushi can be built natively for Windows.\n\nThe suggested audio frontend for Windows is Portaudio (selectable with the `--portaudio` option), which can use most available sound device apis on Windows. You can select which devices to use with the `--audio-input-device` and `--audio-output-device` options. As with Core Audio, to find the id corresponding to a device, launch Sushi with the `--dump-audio-devices` option together with `--portaudio` to get a JSON formatted list printed to stdout.\n\nBy default, Portaudio does not support Asio, to build with Asio support, pass the `-DPA_USE_ASIO=ON` flag to cmake. This will download and build the Asio SDK automatically.\n\nLV2 support is currently not available for Windows.\n\n## Example Sushi configuration files in repository\nUnder `misc/config_files` in this repository, we have a large variety of example Sushi configuration files.\n\nThe first one to try to check if everything is running properly, would be this one that uses the internal sequencer & synthesizer to generate a sequence:\n```\n$ ./sushi --coreaudio -c config_files/play_brickworks_synth.json\n```\n(on Linux with JACK, replace `--coreaudio` with `--jack`).\n\nMany of the examples use the mda-vst3 plugins which are built when building Sushi. If you are running one of the prebuilt packages (available on the releases sections on GitHub), you have everything inside the `sushi` folder there. For example, on macOS you should be able to get a simple working synthesizer with:\n\nOtherwise, if you are building from source, the plugins used by the examples can be found under:\n\n`build/debug/VST3/Debug`, or `build/release/VST3/Release` respectively, for debug and release builds.\n\nTo run Sushi with an example configuration, you simply invoke it while pointing to one of the above paths.\n\nOn Ubuntu that could be:\n```\n$ ./sushi -j -c ../../misc/config_files/play_vst3.json --base-plugin-path VST3/Debug\n```\n\nOr, from a macOS terminal:\n```\n$ ./sushi --coreaudio -c ../../misc/config_files/play_vst3.json --base-plugin-path VST3/Release\n```\n\n## Extra documentation\nConfiguration files are used for global host configs, track and plugins configuration, MIDI routing and mapping, events sequencing.\n\nMore in-depth documentation is available at the [Elk Audio OS official docs page](https://elk-audio.github.io/elk-docs/html/sushi/index.html).\n\n## Building\nSushi builds are supported for native Linux systems, Yocto/OE cross-compiling toolchains targeting Elk Audio OS systems, and macOS.\n\nMake sure that Sushi is cloned with the `--recursive` flag to fetch all required submodules for building. Alternatively run `git submodule update --init --recursive` after cloning.\n\nSushi requires a compiler with support for C++17 features. The recommended compilers are GCC version 10 or higher, and clang version 13 or higher.\n\n### Native Linux dependencies\nSushi handles most dependencies with vcpkg (or as submodules) and will build and link with them automatically. A few dependencies are not included however and must be provided or installed system-wide. See the list below (debian packages names):\n\n  * libasound2-dev\n\n  * libjack-jackd2-dev\n\n  * For VST 2:\n      * Vst 2.4 SDK - Needs to be provided externally as it is not available from Steinberg anymore.\n\n  * For LV2:\n      * liblilv-dev - at least version 0.24.4. Lilv is an official wrapper for LV2.\n      * lilv-utils - at least version 0.24.5.\n      * lv2-dev - at least version 1.18.2. The main LV2 library.\n\n        The official Ubuntu repositories do not have these latest versions at the time of writing. The best source for them is instead the [KX Studio repositories, which you need to enable manually](https://kx.studio/Repositories).\n\n      * For LV2 unit tests:\n          * lv2-examples - at least version 1.18.2.\n          * mda-lv2 - at least version 1.2.4 of [drobilla's port](http://drobilla.net/software/mda-lv2/) - not that from Mod Devices or others.\n\n### Building with vcpkg (native Linux & macOS)\nTo build from a terminal, use the commands below. Vcpkg also integrates nicely with CLion and other IDEs though must manually set the toolchain file.\n\n```\n$ mkdir build && cd build \n$ ../third-party/vcpkg/bootstrap-vcpkg.sh\n$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../third-party/vcpkg/scripts/buildsystems/vcpkg.cmake ..\n$ make \n```\n\nThis might take a while the first time since all the vcpkg dependencies will have to be built first.\n\n**Note:** if using Cmake 4 on MacOS, you might need to set the environment variable SDKROOT to *macosx* in order to build the vcpkg dependencies correctly:\n```\nSDKROOT=macosx cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../third-party/vcpkg/scripts/buildsystems/vcpkg.cmake ..\n```\n### Building with vcpkg (Windows)\nBuilding on Windows is similar to Posix and macOS, with the addition of the triplet configuration.\nTo build from a terminal, use the commands below. Vcpkg also integrates nicely with CLion and Visual Studio, though you must manually set the toolchain file for vcpkg integration.\n\n````\n$ mkdir build ; cd build\n$ ../third-party/vcpkg/bootstrap-vcpkg.bat\n$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=\"../third-party/vcpkg/scripts/buildsystems/vcpkg.cmake\" -DVCPKG_OVERLAY_TRIPLETS=\"../triplets/\" -DVCPKG_TARGET_TRIPLET=win-custom-x86-64 ..\n$ cmake --build ./ --config Release\n````\n\n### Building with Yocto for Elk Audio OS\nSushi can be built either with the provided [Elk Audio OS SDK](https://github.com/elk-audio/elkpi-sdk), or as part of a [full Elk Audio OS image build with bitbake](https://github.com/elk-audio/elk-audio-os-builder).\n\nFollow the instructions in those repositories to set up a cross-compiling SDK and build Sushi for a given target.\n\n### Useful CMake build options\nOption                                | Value    | Notes\n--------------------------------------|----------|------------------------------------------------------------------------------------------------------\nSUSHI_AUDIO_BUFFER_SIZE               | 8 - 512  | The buffer size used in the audio processing. Needs to be a power of 2 (8, 16, 32, 64, 128...).\nSUSHI_WITH_RASPA                      | on / off | Build Sushi with Xenomai RT-kernel support, only for ElkPowered hardware.\nSUSHI_WITH_JACK                       | on / off | Build Sushi with Jack Audio support, only for standard Linux distributions and macOS.\nSUSHI_WITH_PORTAUDIO                  | on / off | Build Sushi with Portaudio support.\nSUSHI_WITH_APPLE_COREAUDIO            | on / off | Build Sushi with Apple Core Audio support.\nSUSHI_WITH_ALSA_MIDI                  | on / off | Build Sushi with Alsa sequencer support for MIDI (Linux only).\nSUSHI_WITH_RT_MIDI                    | on / off | Build Sushi with RtMidi support for MIDI. Cannot be selected if SUSHI_WITH_ALSA_MIDI is set.\nSUSHI_WITH_LINK                       | on / off | Build Sushi with Ableton Link support.\nSUSHI_WITH_VST2                       | on / off | Include support for loading Vst 2.x plugins in Sushi.\nSUSHI_WITH_VST3                       | on / off | Include support for loading Vst 3.x plugins in Sushi.\nSUSHI_LINK_VST3                       | on / off | Link Vst3 SDK (statically) if Vst3 is included. Turn this off if Sushi is linked statically as a library, into an application which already links Vst3's SDK.\nSUSHI_WITH_LV2                        | on / off | Include support for loading LV2 plugins in Sushi.\nSUSHI_WITH_RPC_INTERFACE              | on / off | Build gRPC external control interface, requires gRPC development files.\nSUSHI_BUILD_TWINE                     | on / off | Build and link with the included version of [TWINE](https://github.com/elk-audio/twine), otherwise tries to link with system wide if the option is disabled.\nSUSHI_TWINE_STATIC                    | on / off | Link statically against TWINE (not recommended, useful only in a few cases).\nSUSHI_WITH_UNIT_TESTS                 | on / off | Build and run unit tests together with building Sushi.\nSUSHI_WITH_LV2_MDA_TESTS              | on / off | Include LV2 unit tests which depends on the LV2 drobilla port of the mda plugins being installed. \nSUSHI_VST2_SDK_PATH                   | path     | Path to external Vst 2.4 SDK. Not included and required if WITH_VST2 is enabled.\nSUSHI_WITH_SENTRY                     | on / off | Build Sushi with Sentry error logging support.\nSUSHI_SENTRY_DSN                      | url      | URL to the default value for the Sushi Sentry logging DSN. This can still be passed as a runtime terminal argument.\nSUSHI_DISABLE_MULTICORE_UNIT_TESTS    | on / off | Disable unit-tests dependent on multi-core processing.\nSUSHI_BUILD_STANDALONE_APP            | on / off | Build standalone Sushi executable.\nSUSHI_BUILD_WITH_SANITIZERS           | on / off | Build Sushi with google address sanitizer on. Default is off.\nThe default values for many of the options are platform-specific (native Linux, Yocto/OE, macOS).\n\n_Note_:\n\nbefore version 1.0, the CMake options didn't have the `SUSHI_` prefix. The old names (e.g. `WITH_JACK`) are not supported anymore and should be changed to the new format.\n\n## Running Unit tests separately\nSome Sushi's unit tests depend on test data, which is found through the environment variable `SUSHI_TEST_DATA_DIR`.\nYou will need to define this if you want to run the unit test explicitly, e.g. while debugging:\n\n`$ export SUSHI_TEST_DATA_DIR=/path/to/sushi/repo/test/data`\n\nMoreover, the battery of tests for VST3 plugin support, build and use the example plugins of the VST3 SDK.\nWhile the automated tests running as part of the build find these plugins automatically, when running the `unit_tests` binary manually, you may need to set the working directory to: `/path/to/sushi/build_folder_name/test`,\nfor the path to resolve correctly.\n\n## License\nSushi is licensed under Affero General Public License (“AGPLv3”). See [LICENSE](LICENSE.md) document for the full details of the license. For contributing code to Sushi, see [CONTRIBUTING.md](CONTRIBUTING.md).\n\nCopyright 2017-2023 Elk Audio AB, Stockholm, Sweden.\n\n"
  },
  {
    "path": "apps/CMakeLists.txt",
    "content": "\n######################\n#  Executable target #\n######################\n\nset(APP_FILES main.cpp)\n\nif(APPLE)\n    add_executable(sushi MACOSX_BUNDLE ${APP_FILES})\nelse()\n    add_executable(sushi ${APP_FILES})\nendif()\n\n####################################\n#  Compiler Flags and definitions  #\n####################################\n\nif(MSVC)\n    # C4996: Deprecated warning. This was flooding the terminal,\n    #        I might remove the suppression from here eventually,\n    #        and out into the code again.\n    set(SUSHI_COMPILE_OPTIONS /W4 /fp:fast /wd4996)\n    set(APP_LINK_LIBRARIES ${APP_LINK_LIBRARIES} winmm.lib) # oscpack GetCurrentTimeMs\nelse()\n    set(SUSHI_COMPILE_OPTIONS -Wall -Wextra -Wno-psabi -fPIC -fno-rtti -ffast-math)\nendif()\n\ntarget_compile_options(sushi PRIVATE ${SUSHI_COMPILE_OPTIONS})\n\ntarget_link_libraries(sushi PRIVATE sushi_library ${APP_LINK_LIBRARIES})\n\n####################\n#  Install         #\n####################\n\nset(DOC_FILES_INSTALL\n        \"${PROJECT_SOURCE_DIR}/LICENSE.md\"\n        \"${PROJECT_SOURCE_DIR}/README.md\"\n        \"${PROJECT_SOURCE_DIR}/HISTORY.md\"\n        )\n\nif (APPLE)\n    # TODO: fix to work when not submodule\n    install(CODE [[\n    include(BundleUtilities)\n    fixup_bundle(\"${CMAKE_INSTALL_PREFIX}/sushi/sushi.app\" \"\" \"\")\n    ]] COMPONENT Runtime)\nendif()\n\n### Custom command to copy the dynamic dependencies to the binary folder\n#   Mainly for twine.dll because windows cannot find it from ../twine but\n#   needs it in the same directory\nif (MSVC)\n    add_custom_command(TARGET sushi POST_BUILD\n            COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_RUNTIME_DLLS:sushi> $<TARGET_FILE_DIR:sushi>\n            COMMAND_EXPAND_LISTS)\nendif()\n\ninstall(TARGETS sushi RUNTIME DESTINATION bin BUNDLE DESTINATION Applications)\nforeach(ITEM ${DOC_FILES_INSTALL})\n    install(FILES ${ITEM} DESTINATION share/sushi/doc)\nendforeach()\n"
  },
  {
    "path": "apps/main.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Main entry point to Sushi\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <iostream>\n#include <csignal>\n#include <condition_variable>\n\n#include \"elklog/static_logger.h\"\n\n#include \"sushi/utils.h\"\n#include \"sushi/parameter_dump.h\"\n#include \"sushi/portaudio_devices_dump.h\"\n#include \"sushi/coreaudio_devices_dump.h\"\n#include \"sushi/sushi.h\"\n#include \"sushi/terminal_utilities.h\"\n#include \"sushi/standalone_factory.h\"\n#include \"sushi/offline_factory.h\"\n\nusing namespace sushi;\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"main\");\n\n/**\n * These are used for signalling that Sushi should exit, from across threads.\n * In main(), Sushi waits on this condition variable to exit.\n */\nbool exit_flag = false;\nstd::condition_variable exit_notifier;\n\nbool exit_condition()\n{\n    return exit_flag;\n}\n\n/**\n *  By invoking this method, you can signal to Sushi to exit,\n *  either through passing it to the standard signal(...) method,\n *  or through calling it directly in the code, e.g. when coming upon an unrecoverable error.\n *  When invoking this, Sushi will still wind down, cleanly close allocated resources, and flush logs.\n * @param sig the signal, e.g. SIGINT, SIGTERM.\n */\nvoid exit_on_signal([[maybe_unused]] int sig)\n{\n    exit_flag = true;\n    exit_notifier.notify_one();\n}\n\n/**\n * If the error encountered is so severe as to require immediate exit, invoke this, instead of exit_on_signal.\n * @param message\n */\nvoid error_exit(const std::string& message, sushi::Status status)\n{\n    std::cerr << message << std::endl;\n    int error_code = static_cast<int>(status);\n    std::exit(error_code);\n}\n\n/**\n * Tries to start Sushi\n * @param options a SushiOptions structure\n * @return a Sushi instance, if successful - if not, the unique_ptr is empty.\n */\nstd::unique_ptr<Sushi> start_sushi(SushiOptions options);\n\nvoid pipe_signal_handler([[maybe_unused]] int sig)\n{\n    ELKLOG_LOG_INFO(\"Pipe signal received and ignored: {}\", sig);\n}\n\nstatic void print_sushi_headline()\n{\n    std::cout << \"SUSHI - Copyright 2017-2023 Elk Audio AB, Stockholm\" << std::endl;\n    std::cout << \"SUSHI is licensed under the Affero GPL 3.0. Source code is available at github.com/elk-audio\" << std::endl;\n}\n\nint main(int argc, char* argv[])\n{\n    signal(SIGINT, exit_on_signal);\n    signal(SIGTERM, exit_on_signal);\n#ifndef _MSC_VER\n    signal(SIGPIPE, pipe_signal_handler);\n#endif\n    \n    // option_parser accepts arguments excluding program name,\n    // so skip it if it is present.\n    if (argc > 0)\n    {\n        argc--;\n        argv++;\n    }\n\n    SushiOptions options;\n\n    options.config_source = sushi::ConfigurationSource::FILE;\n\n    auto option_status = parse_options(argc, argv, options);\n    if (option_status == ParseStatus::ERROR)\n    {\n        return 1;\n    }\n    else if (option_status == ParseStatus::MISSING_ARGUMENTS)\n    {\n        return 2;\n    }\n    else if (option_status == ParseStatus::EXIT)\n    {\n        return 0;\n    }\n\n    init_logger(options);\n\n    if (options.enable_audio_devices_dump)\n    {\n        if (options.frontend_type == FrontendType::PORTAUDIO)\n        {\n#ifdef SUSHI_BUILD_WITH_PORTAUDIO\n            std::cout << sushi::generate_portaudio_devices_info_document() << std::endl;\n            return 0;\n#else\n            std::cerr << \"SUSHI not built with Portaudio support, cannot dump devices.\" << std::endl;\n#endif\n        }\n        else if (options.frontend_type == FrontendType::APPLE_COREAUDIO)\n        {\n#ifdef SUSHI_BUILD_WITH_APPLE_COREAUDIO\n            std::cout << sushi::generate_coreaudio_devices_info_document() << std::endl;\n#else\n            std::cerr << \"SUSHI not built with Apple CoreAudio support, cannot dump devices.\" << std::endl;\n#endif\n            return 0;\n        }\n        else\n        {\n            std::cout << \"No frontend specified or specified frontend not supported (please specify .\" << std::endl;\n            return 1;\n        }\n    }\n\n    auto sushi = start_sushi(options);\n\n    if (sushi == nullptr)\n    {\n        return -1;\n    }\n\n    if (options.frontend_type != FrontendType::OFFLINE)\n    {\n        std::mutex m;\n        std::unique_lock<std::mutex> lock(m);\n        exit_notifier.wait(lock, exit_condition);\n    }\n\n    sushi->stop();\n\n    ELKLOG_LOG_INFO(\"Sushi exiting normally!\");\n\n    return 0;\n}\n\nstd::unique_ptr<Sushi> start_sushi(SushiOptions options)\n{\n    std::unique_ptr<FactoryInterface> factory;\n\n    if (options.frontend_type == FrontendType::DUMMY ||\n        options.frontend_type == FrontendType::OFFLINE)\n    {\n        factory = std::make_unique<OfflineFactory>();\n    }\n    else if (options.frontend_type == FrontendType::JACK ||\n             options.frontend_type == FrontendType::XENOMAI_RASPA ||\n             options.frontend_type == FrontendType::APPLE_COREAUDIO ||\n             options.frontend_type == FrontendType::PORTAUDIO)\n    {\n        factory = std::make_unique<StandaloneFactory>();\n    }\n    else\n    {\n        error_exit(\"Invalid frontend configuration. Reactive, or None, are not supported when standalone.\",\n                   Status::FRONTEND_IS_INCOMPATIBLE_WITH_STANDALONE);\n    }\n\n    // Initialising:\n\n    auto [sushi, status] = factory->new_instance(options);\n\n    if (status == Status::FAILED_OSC_FRONTEND_INITIALIZATION)\n    {\n        error_exit(\"Instantiating OSC server on port \" + std::to_string(options.osc_server_port) + \" failed.\",\n                   status);\n    }\n    else if (status != Status::OK)\n    {\n        auto message = to_string(status);\n        if (status == Status::FAILED_INVALID_FILE_PATH)\n        {\n            message.append(options.config_filename);\n        }\n\n        error_exit(message, status);\n    }\n\n    if (options.enable_parameter_dump)\n    {\n        std::cout << sushi::generate_processor_parameter_document(sushi->controller());\n        std::cout << \"Parameter dump completed - exiting.\" << std::endl;\n        std::exit(EXIT_SUCCESS);\n    }\n    else\n    {\n        print_sushi_headline();\n    }\n\n    // ...and starting:\n\n    auto start_status = sushi->start();\n\n    if (start_status == Status::OK)\n    {\n        return std::move(sushi);\n    }\n    else if (start_status == Status::FAILED_TO_START_RPC_SERVER)\n    {\n        sushi.reset();\n\n        error_exit(\"Failure starting gRPC server on address \" + options.grpc_listening_address, status);\n    }\n\n    return nullptr;\n}"
  },
  {
    "path": "bitbucket-pipelines.yml",
    "content": "# Only use spaces to indent your .yml configuration.\n# -----\n# You can specify a custom docker image from Docker Hub as your build environment.\nimage: ubuntu:24.04\n\npipelines:\n  default:\n    - step:\n        name: Build sushi + tests\n\n        size: 4x\n\n        caches:\n          - vcpkg\n\n        script:\n          # Prepare system.\n          - apt-get update -y\n          - apt install build-essential -y\n          - apt-get install -y -q libjack-jackd2-dev liblo-dev\n          - apt-get install -y -q libsndfile1-dev libasound2-dev\n          - apt-get install -y -q cmake git make\n          - apt-get install -y -q curl zip unzip tar\n          - apt-get install -y -q libgrpc++-dev protobuf-compiler-grpc\n          - apt-get install -y -q liblilv-dev lilv-utils lv2-dev lv2-examples mda-lv2\n          - apt-get install -y software-properties-common\n          - add-apt-repository -y ppa:ubuntu-toolchain-r/test\n          - export LV2_PATH=$HOME/.lv2:/usr/local/lib/lv2:/usr/lib/lv2\n\n          # Prepare dependencies\n          # Rewrite the relative submodule url as bitbucket doesn't handle relative paths correctly\n          - sed -i 's/url = \\.\\.\\(.*\\)/url = git@bitbucket.org:'$BITBUCKET_TEAM'\\1/g' .gitmodules\n          - git submodule update --init --recursive\n          # Set test data dir (for running unit test manually)\n          - export SUSHI_TEST_DATA_DIR=$PWD/test/data\n          # Set the number of cores to use when building\n          - export BUILD_CORES=8\n          # Boostrap vcpkg\n          - export VCPKG_BINARY_SOURCES=\"clear;files,$(pwd)/vcpkgcache,readwrite\"\n          - ./third-party/vcpkg/bootstrap-vcpkg.sh\n          - mkdir build\n          - cd build\n          - cmake -DCMAKE_TOOLCHAIN_FILE=../third-party/vcpkg/scripts/buildsystems/vcpkg.cmake -DWITH_XENOMAI=FALSE -DWITH_JACK=TRUE -DWITH_VST2=OFF -DSUSHI_WITH_PORTAUDIO=ON -DWITH_LV2=ON -DWITH_LV2_MDA_TESTS=ON -DTWINE_WITH_XENOMAI=OFF -DSUSHI_BUILD_WITH_SANITIZERS=OFF ..\n\n          # Build multiple configuration and run unit tests\n          # Build sushi and unit tests with default options and default buffer size (64 samples)\n          - make sushi -j${BUILD_CORES}\n          - make unit_tests -j${BUILD_CORES}\n          # Run unit tests with some tests test disabled\n          - cd test\n          - ./unit_tests --gtest_filter=-TestAudioGraph.TestMultiCoreOperation:TestLv2Wrapper.TestSynchronousStateAndWorkerThreads:TestLv2Wrapper.TestMidiEventInputAndOutput\n          - cd ..\n\n          # Build with buffers size 32 samples\n          - cmake -DSUSHI_AUDIO_BUFFER_SIZE=32 ..\n          - make sushi -j${BUILD_CORES}\n          - make unit_tests -j${BUILD_CORES}\n          - cd test\n          - ./unit_tests --gtest_filter=-TestAudioGraph.TestMultiCoreOperation:TestLv2Wrapper.TestSynchronousStateAndWorkerThreads:TestLv2Wrapper.TestMidiEventInputAndOutput\n          - cd ..\n\n          # Build with buffer size 128 samples\n          - cmake -DSUSHI_AUDIO_BUFFER_SIZE=128 ..\n          - make sushi -j${BUILD_CORES}\n          - make unit_tests -j${BUILD_CORES}\n          - cd test\n          - ./unit_tests --gtest_filter=-TestAudioGraph.TestMultiCoreOperation:TestLv2Wrapper.TestSynchronousStateAndWorkerThreads:TestLv2Wrapper.TestMidiEventInputAndOutput\n          - cd ..\n\n          # Quick smoke test (not really working yet)\n          #- ./sushi -d -c ../../misc/config_files/config_play.json&\n          #- sleep 6\n          #- pkill sushi\n\n          # Build with most options disabled to test compile time configuration\n          - cmake -DAUDIO_BUFFER_SIZE=64 -DSUSHI_WITH_VST2=OFF -DSUSHI_WITH_VST3=OFF -DSUSHI_WITH_LV2=OFF -DSUSHI_WITH_LV2_MDA_TESTS=OFF -DSUSHI_WITH_RT_MIDI=ON -DSUSHI_WITH_ALSA_MIDI=OFF -DSUSHI_WITH_PORTAUDIO=OFF ..\n          - make sushi -j4\n          - make unit_tests -j${BUILD_CORES}\n          - cd test\n          - ./unit_tests --gtest_filter=-TestAudioGraph.TestMultiCoreOperation\n          - cd ..\n\ndefinitions:\n  caches:\n    vcpkg: vcpkgcache\n"
  },
  {
    "path": "docs/LIBRARY.md",
    "content": "# Sushi as a Library\nUsing Sushi as a statically linked library is a newly developed feature.\n\nWe have considered two primary use-cases for Sushi as a library:\n\n**Reactive:** \n\nSushi running inside a plugin / other audio host, which provides the audio and MIDI frontends, passing on audio and MIDI to Sushi.\n\n**Active:** \n\nAllowing the creation of standalone audio-hosts, which embed Sushi, but expand on the functionality of the terminal application we provide in `/apps`.\n\nFor the Reactive use-case, we have developed new frontends for audio and MIDI in Sushi, designed to be Reactive, as well as a new \"Real-Time\" Controller interface using which the necessary Sushi control is available in the audio callback. These are still in their first incarnation, and while they are definitely usable in production, they are currently missing features. That’s also why these files may still contain “TODO’s” as placeholders - to be removed as the features they refer to are added.\n\nFor the Active use-case, there are no such limitations - the full Sushi functionality is available. \n\n## The main limitations for the Reactive use-case are currently that:\n* Sushi can only work with stereo audio I/O.\n* Sushi's audio buffer size is set at compile time, using the CMake argument SUSHI_AUDIO_BUFFER_SIZE. But an audio host may have a dynamic buffer-size setting. If the buffer size doesn't match, the host needs to handle that, ensuring Sushi is only ever given audio buffers of the size defined at build-time.\n* MIDI I/O to Sushi from a containing host application is not currently real-time, but asynchronous, meaning MIDI and audio synchronisation is not sample-accurate.\n* For any control commands to be processes by Sushi, it needs to be receiving audio buffers regularly. When no audio callbacks are received, Sushi will also not process and control commands (e.g. those received over gRPC and OSC).\n\n## Using Sushi as a library\nFor using Sushi as a library, you only need to be aware of the header files contained in the include/sushi folder, where Sushi's API is exposed. The internals are hidden away.\n\nSome of those headers are useful only when Sushi is Passive, others only when it's Active, and others for both cases.\n\n## Common API for Reactive and Active\n\nSushi has several compile-time arguments and dependencies, which apply equally to the library and standalone use. These are documented in the main [README.md](../README.md) of this repository, and will not be repeated here.\n\n### Instantiation\n\nThe main Sushi API is defined in sushi.h.\n\nAll options for instantiating Sushi are collected in the struct **SushiOptions**. Some of its fields are enum classes - which are then also defined in sushi.h.\n\nThis file also contains the main abstract base-class for Sushi, which however cannot be instantiated directly. Instead, one of the provided factories needs to be used. Simply create a factory, and invoke the below method, passing your populated SushiOptions struct:\n\n```c++\nstd::pair<std::unique_ptr<Sushi>, Status> new_instance(SushiOptions& options) override;\n```\n\nIt will return a std::pair, of either a valid Sushi instance, and Status::OK, or an empty std::unique_ptr, and the Status of why instantiation failed.\n\nNote that the SushiOptions is passed by reference to new_instance, because its contents are *suggestions*, which may be overridden if needed - in which case your options struct instance will be updated to reflect the changes made.\n\n### Controlling during run-time\n\n#### C++ Control API\n\nThe main control interface is exposed in **control_interface.h**.\n\nNote that none of those classes can be instantiated directly. They are created internally in Sushi, and their instances are accessed through the **Sushi::Controller()** method, which returns a pointer to a **SushiControl** instance, that in turn will hold instances for all the interfaces defined in control_interface.h:\n\n```C++\ncontrol::SushiControl* controller();\n```\n\nIt's important to note that none of these interfaces are real-time safe. That means they are not safely callable from within an audio-callback, but should be invoked from a separate thread altogether. Sushi then schedules their effect internally so that they do not interfere with the real-time audio callback.\n\nThe C++ control API in control_interface.h is extensive and will not be further documented here. It is written so that it should be intuitively self-explanatory.\n\n#### Remote Control API\n\nAdditionally, Sushi can be controlled over gRPC and OSC, which are both unchanged in the \"sushi as library\" implementation - see the documentation for this in the main [README.md](../README.md) of this repository, and the [Elk Audio GitHub page with detaled documentation](https://elk-audio.github.io/elk-docs/html/index.html).\n\nOnce Sushi is instantiated, subsequent steps vary depending on the use-case.\n\n## Reactive use-case\n\nYou will need the following fields:\n\n```C++\nsushi::SushiOptions _sushi_options;\nstd::unique_ptr<sushi::RtController> _rt_controller;\nstd::unique_ptr<sushi::Sushi> _sushi;\nsushi::ChunkSampleBuffer _sushi_buffer_in;\nsushi::ChunkSampleBuffer _sushi_buffer_out;\n```\n\n### Starting Reactive Sushi\n\nFor the Reactive use-case, fetch the **RtController** from a **ReactiveFactory** instance (Sushi factories are single-use). Assuming Sushi has started successfully, this will be a non-null std::unique_ptr, meaning you are taking over the ownership of the controller.\n\nRtController is named as such because its methods are safe to invoke during a real-time audio callback.\n\n```C++\nsushi::ReactiveFactory factory;\nauto [sushi, status] = factory.new_instance(_sushi_options);\n_sushi = std::move (sushi);\n_rt_controller = factory.rt_controller();\n```\n\nIf you are going to use MIDI, pass a callback Lambda to it through your RtController instance, so that Sushi can output MIDI data to your host:\n\n```C++\n_rt_controller->set_midi_callback(\n    [&] (int output, sushi::MidiDataByte data, sushi::Time /*timestamp*/)\n    { \n    \t\t// Handle MIDI data.\n    }\n);\n```\n\nIf you're going to host Sushi in e.g. an audio plugin, which can pass a transport position to Sushi, you need to tell Sushi to not calculate its own transport position internally:\n\n```C++\n_rt_controller->set_position_source (sushi::TransportPositionSource::EXTERNAL);\n```\n\nFinally, start Sushi, and if successful, set the sample rate. If not, address any issue and retry:\n\n```c++\n_sushi_start_status = _sushi->start();\nif (_sushi_start_status == sushi::Status::OK)\n{\n    _sushi->set_sample_rate(_sample_rate);\n    return true; // Sushi is started.\n}\nelse\n{\n    _sushi = nullptr;\n    _rt_controller = nullptr;\n    // The factory is single use. Address the error, and perhaps retry, with a new factory instance.\n}\n```\n\nFor example: If you are using gRPC, it is possible that Sushi failed to start because the configured gRPC port was taken. You can then increment the port number using the utility method provided, and retry.\n\n```C++\nbool incrementation_status = _sushi_options.increment_grpc_port_number();\nif (!incrementation_status)\n{\n    // Starting Sushi failed: gRPC address is malformed.\n    break;\n}\n```\n\n### Invoking Sushi in the audio callback of your host\n\nFor each audio callback invocation, you need to do the following, always using the RtController  instance.\n\nPass MIDI to Sushi using receive_midi.\n\n```C++\nvoid receive_midi(int input, MidiDataByte data, Time timestamp);\n```\n\nIf you're using an external transport position source: update the Sushi transport, using the following.\n\n```C++\nvoid set_tempo(float tempo);\nvoid set_time_signature(control::TimeSignature time_signature);\nvoid set_playing_mode(control::PlayingMode mode);\nbool set_current_beats(double beat_time);\nbool set_current_bar_beats(double bar_beat_count);\n```\n\nFill your sushi::**ChunkSampleBuffer** input audio buffer, and call process_audio on the RtController.\n\n```C++\nvoid process_audio(ChunkSampleBuffer& in_buffer,\n                   ChunkSampleBuffer& out_buffer,\n                   Time timestamp)\n```\n\nThis will populate the out_buffer. The sushi::**Time** `timestamp` may either be calculated using your hosts playhead data, or you need to use the RtController utility to calculate it, if you're using the Sushi internal transport.\n\n```C++\nsushi::Time calculate_timestamp_from_start(float sample_rate) const\n```\n\nIn which case, after the process_audio call, you will then also need to increment_samples_since_start.\n\n```C++\nvoid increment_samples_since_start(uint64_t sample_count, Time timestamp)\n```\n\nThere is more detail to the above process to handle e.g. MIDI, or non-matching buffer sizes. We will be publishing a concrete example, showing how to implement a concrete audio plugin which wraps Sushi, in due course.\n\n### Stopping Reactive Sushi\n\nTo stop Sushi, you'll really only need to delete the controller, call stop() on your sushi instance, and then delete that too:\n\n```C++\nif (_sushi)\n{\n    _rt_controller.reset();\n    _sushi->stop();\n    _sushi.reset();\n}\n```\n\n## Active use-case\n\nFor the active use-case the Sushi API footprint is a bit smaller. You will need the following fields.\n\n```c++\nSushiOptions options;\nstd::unique_ptr<FactoryInterface> factory;\nstd::unique_ptr<Sushi> sushi;\n```\n\nThen depending on whether you plan to use an `OFFLINE` frontend, or if you want Sushi to instantiate its own Audio frontend (the options are defined in sushi::FrontendType), you will instantiate either a **StandaloneFactory**.\n\n```C++\nfactory = std::make_unique<StandaloneFactory>();\n```\n\nOr an **OfflineFactory**.\n\n```C++\nfactory = std::make_unique<OfflineFactory>();\n```\n\nTo populate SushiOptions, we provide a utility method for parsing options from the Sushi terminal arguments, in **terminal_utilities.h**.\n\n```C++\nParseStatus parse_options(int argc, char* argv[], sushi::SushiOptions& options);\n```\n\nFrom there on, you instantiate Sushi.\n\n```C++\nauto [sushi, status] = factory->new_instance(options);\n```\n\nAnd on success, start it.\n\n```C++\nauto start_status = sushi->start();\n```\n\nFrom there on, you can wait in the creating thread, until the application needs to exit, at which point you just need to invoke stop().\n\n```C++\nsushi->stop();\n```\n\n## Examples\n\nFor examples of sushi as a library:\n\n### Active use-case\n\nSee apps/main.cpp in this repository, which shows a terminal application use-case.\n\n### Passive use-case\n\nSee the example JUCE-based project, residing in a dedicated repository, where we demonstrate how Sushi can be wrapped in a JUCE audio plugin."
  },
  {
    "path": "include/sushi/compile_time_settings.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Methods for querying compile - time configuration (build options, version, githash, etc).\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_COMPILE_TIME_SETTINGS_H\n#define SUSHI_COMPILE_TIME_SETTINGS_H\n\n#include <string>\n#include <array>\n\n#include \"options.h\"\n\n#include \"generated/version.h\"\n\n// For AUDIO_CHUNK_SIZE.\n#include \"constants.h\"\n\nnamespace sushi {\n\nstruct CompileTimeSettings\n{\n    static constexpr auto sushi_version = SUSHI_STRINGIZE(SUSHI__VERSION_MAJ) \".\" SUSHI_STRINGIZE(SUSHI__VERSION_MIN) \".\" SUSHI_STRINGIZE(SUSHI__VERSION_REV);\n\n    static constexpr auto sushi_api_version = SUSHI_EXTERNAL_API_VERSION;\n\n    static constexpr auto git_commit_hash = SUSHI_GIT_COMMIT_HASH;\n\n    static constexpr auto build_timestamp = SUSHI_BUILD_TIMESTAMP;\n\n    static constexpr auto audio_chunk_size = AUDIO_CHUNK_SIZE;\n\n    static constexpr std::array enabled_build_options = {\n#ifdef SUSHI_BUILD_WITH_VST2\n            \"vst2\",\n#endif\n#ifdef SUSHI_BUILD_WITH_VST3\n            \"vst3\",\n#endif\n#ifdef SUSHI_BUILD_WITH_LV2\n            \"lv2\",\n#endif\n#ifdef SUSHI_BUILD_WITH_JACK\n            \"jack\",\n#endif\n#ifdef SUSHI_BUILD_WITH_RASPA\n            \"raspa\",\n#endif\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n            \"rpc control\",\n#endif\n#ifdef SUSHI_BUILD_WITH_ABLETON_LINK\n            \"ableton link\",\n#endif\n#ifdef SUSHI_BUILD_WITH_SENTRY\n            \"sentry\"\n#endif\n\n// Without this entry, if all ifdefs are false, compiling will fail.\n// Not as uncommon as you may think: building without LV2, VST2 and VST3 causes this for unit-tests.\n#if    !defined (SUSHI_BUILD_WITH_VST2) \\\n    && !defined (SUSHI_BUILD_WITH_VST3) \\\n    && !defined (SUSHI_BUILD_WITH_LV2)  \\\n    && !defined (SUSHI_BUILD_WITH_JACK) \\\n    && !defined (SUSHI_BUILD_WITH_RASPA)\\\n    && !defined (SUSHI_BUILD_WITH_RPC_INTERFACE) \\\n    && !defined (SUSHI_BUILD_WITH_ABLETON_LINK)  \\\n    && !defined (SUSHI_BUILD_WITH_SENTRY)\n            \"\"\n#endif\n    };\n};\n\n} // end namespace sushi\n\n#endif // SUSHI_COMPILE_TIME_SETTINGS_H"
  },
  {
    "path": "include/sushi/constants.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Compile time constants\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <chrono>\n\n#ifndef SUSHI_CONSTANTS_H\n#define SUSHI_CONSTANTS_H\n\nnamespace sushi {\n\n/* The number of samples to process in one chunk. It is defined as a\ncompile-time constant to give more room for optimizations */\n#ifdef SUSHI_CUSTOM_AUDIO_CHUNK_SIZE\nconstexpr int AUDIO_CHUNK_SIZE = SUSHI_CUSTOM_AUDIO_CHUNK_SIZE;\n#else\nconstexpr int AUDIO_CHUNK_SIZE = 64;\n#endif\n\nconstexpr int MAX_ENGINE_CV_IO_PORTS = 4;\nconstexpr int MAX_ENGINE_GATE_PORTS = 8;\nconstexpr int MAX_ENGINE_GATE_NOTE_NO = 127;\n\nconstexpr int MAX_TRACK_CHANNELS = 16;\n\nconstexpr float PAN_GAIN_3_DB = 1.412537f;\nconstexpr auto GAIN_SMOOTHING_TIME = std::chrono::milliseconds(20);\n\nconstexpr int SUSHI_PPQN_TICK = 24;\n\n/* Use in class declaration to disallow copying of this class.\n * Note that this marks copy constructor and assignment operator\n * as deleted and hence their r-value counterparts are not generated.\n *\n * In order to make a class moveable though still non-copyable,\n * implement a move constructor and move assignment operator. Default\n * copy constructor will then not be generated. Usage of this macro is\n * in this case not necessary to make the class non-copyable. But can\n * still be used for clarity.\n */\n#define SUSHI_DECLARE_NON_COPYABLE(type) type(const type& other) = delete; \\\n                                         type& operator=(const type&) = delete;\n\n} // end namespace sushi\n\n#endif //SUSHI_CONSTANTS_H\n"
  },
  {
    "path": "include/sushi/control_interface.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Abstract interface for external control of sushi over rpc, osc or similar\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_CONTROL_INTERFACE_H\n#define SUSHI_CONTROL_INTERFACE_H\n\n#include <utility>\n#include <memory>\n#include <string>\n#include <optional>\n#include <vector>\n#include <chrono>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#ifdef ERROR\n#undef ERROR\n#endif\n\nELK_PUSH_WARNING\nELK_DISABLE_TRIVIAL_DESTRUCTOR_NOT_VIRTUAL\n\nnamespace sushi::control {\n\nusing Time = std::chrono::microseconds;\n\nenum class ControlStatus\n{\n    OK,\n    ASYNC_RESPONSE,\n    ERROR,\n    UNSUPPORTED_OPERATION,\n    NOT_FOUND,\n    OUT_OF_RANGE,\n    INVALID_ARGUMENTS\n};\n\nenum class PlayingMode\n{\n    STOPPED,\n    PLAYING,\n    RECORDING\n};\n\nenum class SyncMode\n{\n    INTERNAL,\n    MIDI,\n    GATE,\n    LINK\n};\n\nstruct ControlResponse\n{\n    ControlStatus status;\n    int           id;\n};\n\nstruct TimeSignature\n{\n    int numerator;\n    int denominator;\n};\n\nstruct Timings\n{\n    float avg;\n    float min;\n    float max;\n};\n\nstruct CpuTimings\n{\n    Timings main;\n    std::vector<Timings> threads;\n};\n\nenum class PluginType\n{\n    INTERNAL,\n    VST2X,\n    VST3X,\n    LV2\n};\n\nenum class ParameterType\n{\n    BOOL,\n    INT,\n    FLOAT\n};\n\nstruct ParameterInfo\n{\n    int             id;\n    ParameterType   type;\n    std::string     label;\n    std::string     name;\n    std::string     unit;\n    bool            automatable;\n    float           min_domain_value;\n    float           max_domain_value;\n};\n\nstruct PropertyInfo\n{\n    int         id;\n    std::string name;\n    std::string label;\n};\n\nstruct ProcessorInfo\n{\n    int         id;\n    std::string label;\n    std::string name;\n    int         parameter_count;\n    int         program_count;\n};\n\nstruct ProgramInfo\n{\n    int         id;\n    std::string name;\n};\n\nenum class TrackType\n{\n    REGULAR,\n    PRE,\n    POST\n};\n\nstruct TrackInfo\n{\n    int         id;\n    std::string label;\n    std::string name;\n    int         channels;\n    int         buses;\n    int         thread;\n    TrackType   type;\n    std::vector<int> processors;\n};\n\nstruct ProcessorState\n{\n    std::optional<bool> bypassed;\n    std::optional<int>  program;\n    std::vector<std::pair<int, float>> parameters;\n    std::vector<std::pair<int, std::string>> properties;\n    std::vector<std::byte> binary_data;\n};\n\nstruct SushiBuildInfo\n{\n    std::string                 version;\n    std::vector<std::string>    build_options;\n    int                         audio_buffer_size;\n    std::string                 commit_hash;\n    std::string                 build_date;\n};\n\nenum class MidiChannel\n{\n    MIDI_CH_1,\n    MIDI_CH_2,\n    MIDI_CH_3,\n    MIDI_CH_4,\n    MIDI_CH_5,\n    MIDI_CH_6,\n    MIDI_CH_7,\n    MIDI_CH_8,\n    MIDI_CH_9,\n    MIDI_CH_10,\n    MIDI_CH_11,\n    MIDI_CH_12,\n    MIDI_CH_13,\n    MIDI_CH_14,\n    MIDI_CH_15,\n    MIDI_CH_16,\n    MIDI_CH_OMNI\n};\n\nstruct AudioConnection\n{\n    int track_id;\n    int track_channel;\n    int engine_channel;\n};\n\nstruct CvConnection\n{\n    int track_id;\n    int parameter_id;\n    int cv_port_id;\n};\n\nstruct GateConnection\n{\n    int processor_id;\n    int gate_port_id;\n    int channel;\n    int note_no;\n};\n\nstruct MidiKbdConnection\n{\n    int         track_id;\n    MidiChannel channel;\n    int         port;\n    bool        raw_midi;\n};\n\nstruct MidiCCConnection\n{\n    int         processor_id;\n    int         parameter_id;\n    MidiChannel channel;\n    int         port;\n    int         cc_number;\n    int         min_range;\n    int         max_range;\n    bool        relative_mode;\n};\n\nstruct MidiPCConnection\n{\n    int         processor_id;\n    MidiChannel channel;\n    int         port;\n};\n\nenum class NotificationType\n{\n    TRANSPORT_UPDATE,\n    CPU_TIMING_UPDATE,\n    TRACK_UPDATE,\n    PROCESSOR_UPDATE,\n    PARAMETER_CHANGE,\n    PROPERTY_CHANGE,\n    ASYNC_COMMAND_COMPLETION\n};\n\nenum class ProcessorAction\n{\n    ADDED,\n    DELETED\n};\n\nenum class TrackAction\n{\n    ADDED,\n    DELETED\n};\n\nenum class TransportAction\n{\n    PLAYING_MODE_CHANGED,\n    SYNC_MODE_CHANGED,\n    TIME_SIGNATURE_CHANGED,\n    TEMPO_CHANGED\n};\n\nstruct MidiKbdConnectionState\n{\n    std::string track;\n    MidiChannel channel;\n    int         port;\n    bool        raw_midi;\n};\n\nstruct MidiCCConnectionState\n{\n    std::string processor;\n    int         parameter_id;\n    MidiChannel channel;\n    int         port;\n    int         cc_number;\n    float       min_range;\n    float       max_range;\n    bool        relative_mode;\n};\n\nstruct MidiPCConnectionState\n{\n    std::string processor;\n    MidiChannel channel;\n    int         port;\n};\n\nstruct MidiState\n{\n    int inputs;\n    int outputs;\n    std::vector<MidiKbdConnectionState> kbd_input_connections;\n    std::vector<MidiKbdConnectionState> kbd_output_connections;\n    std::vector<MidiCCConnectionState> cc_connections;\n    std::vector<MidiPCConnectionState> pc_connections;\n    std::vector<int> enabled_clock_outputs;\n};\n\nstruct OscParameterState\n{\n    std::string processor;\n    std::vector<int> parameter_ids;\n};\n\nstruct OscState\n{\n    bool enable_all_processor_outputs;\n    std::vector<OscParameterState> enabled_processor_outputs;\n};\n\nstruct TrackAudioConnectionState\n{\n    std::string  track;\n    int          track_channel;\n    int          engine_channel;\n};\n\nstruct EngineState\n{\n    float           sample_rate;\n    float           tempo;\n    PlayingMode     playing_mode;\n    SyncMode        sync_mode;\n    TimeSignature   time_signature;\n    bool            input_clip_detection;\n    bool            output_clip_detection;\n    bool            master_limiter;\n    int             used_audio_inputs;\n    int             used_audio_outputs;\n    std::vector<TrackAudioConnectionState> input_connections;\n    std::vector<TrackAudioConnectionState> output_connections;\n};\n\nstruct PluginClass\n{\n    std::string     name;\n    std::string     label;\n    std::string     uid;\n    std::string     path;\n    PluginType      type;\n    ProcessorState  state;\n};\n\nstruct TrackState\n{\n    std::string     name;\n    std::string     label;\n    int             channels;\n    int             buses;\n    int             thread;\n    TrackType       type;\n    ProcessorState  track_state;\n    std::vector<PluginClass>    processors;\n};\n\nstruct SessionState\n{\n    SushiBuildInfo      sushi_info;\n    std::string         save_date;\n    OscState            osc_state;\n    MidiState           midi_state;\n    EngineState         engine_state;\n    std::vector<TrackState> tracks;\n};\n\nclass SystemController\n{\npublic:\n    virtual ~SystemController() = default;\n\n    [[nodiscard]] virtual std::string    get_sushi_version() const = 0;\n    [[nodiscard]] virtual std::string    get_sushi_api_version() const = 0;\n    [[nodiscard]] virtual SushiBuildInfo get_sushi_build_info() const = 0;\n    [[nodiscard]] virtual int            get_input_audio_channel_count() const = 0;\n    [[nodiscard]] virtual int            get_output_audio_channel_count() const = 0;\n\nprotected:\n    SystemController() = default;\n};\n\nclass TransportController\n{\npublic:\n    virtual ~TransportController() = default;\n\n    [[nodiscard]] virtual float           get_samplerate() const = 0;\n    [[nodiscard]] virtual PlayingMode     get_playing_mode() const = 0;\n    [[nodiscard]] virtual SyncMode        get_sync_mode() const = 0;\n    [[nodiscard]] virtual TimeSignature   get_time_signature() const = 0;\n    [[nodiscard]] virtual float           get_tempo() const = 0;\n\n    virtual ControlStatus       set_sync_mode(SyncMode sync_mode) = 0;\n    virtual void                set_playing_mode(PlayingMode playing_mode) = 0;\n    virtual ControlStatus       set_tempo(float tempo) = 0;\n    virtual ControlStatus       set_time_signature(TimeSignature signature) = 0;\n\nprotected:\n    TransportController() = default;\n};\n\nclass TimingController\n{\npublic:\n    virtual ~TimingController() = default;\n\n    [[nodiscard]] virtual bool                      get_timing_statistics_enabled() const = 0;\n    virtual void                                    set_timing_statistics_enabled(bool enabled) = 0;\n\n    [[nodiscard]] virtual std::pair<ControlStatus, CpuTimings> get_engine_timings() const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, Timings>    get_track_timings(int track_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, Timings>    get_processor_timings(int processor_id) const = 0;\n\n    virtual ControlStatus      reset_all_timings() = 0;\n    virtual ControlStatus      reset_track_timings(int track_id) = 0;\n    virtual ControlStatus      reset_processor_timings(int processor_id) = 0;\n\nprotected:\n    TimingController() = default;\n};\n\nclass KeyboardController\n{\npublic:\n    virtual ~KeyboardController() = default;\n\n    virtual ControlStatus send_note_on(int track_id, int channel, int note, float velocity) = 0;\n    virtual ControlStatus send_note_off(int track_id, int channel, int note, float velocity) = 0;\n    virtual ControlStatus send_note_aftertouch(int track_id, int channel, int note, float value) = 0;\n    virtual ControlStatus send_aftertouch(int track_id, int channel, float value) = 0;\n    virtual ControlStatus send_pitch_bend(int track_id, int channel, float value) = 0;\n    virtual ControlStatus send_modulation(int track_id, int channel, float value) = 0;\n\nprotected:\n    KeyboardController() = default;\n};\n\nclass AudioGraphController\n{\npublic:\n    virtual ~AudioGraphController() = default;\n\n    [[nodiscard]] virtual std::vector<ProcessorInfo>                           get_all_processors() const = 0;\n    [[nodiscard]] virtual std::vector<TrackInfo>                               get_all_tracks() const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, int>                        get_track_id(const std::string& track_name) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, TrackInfo>                  get_track_info(int track_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<ProcessorInfo>> get_track_processors(int track_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, int>                        get_processor_id(const std::string& processor_name) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, ProcessorInfo>              get_processor_info(int processor_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, bool>                       get_processor_bypass_state(int processor_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, ProcessorState>             get_processor_state(int processor_id) const = 0;\n\n    virtual ControlResponse set_processor_bypass_state(int processor_id, bool bypass_enabled) = 0;\n    virtual ControlResponse set_processor_state(int processor_id, const ProcessorState& state) = 0;\n\n    virtual ControlResponse create_track(const std::string& name, int channels, std::optional<int> thread) = 0;\n    virtual ControlResponse create_multibus_track(const std::string& name, int buses, std::optional<int> thread) = 0;\n    virtual ControlResponse create_pre_track(const std::string& name) = 0;\n    virtual ControlResponse create_post_track(const std::string& name) = 0;\n    virtual ControlResponse move_processor_on_track(int processor_id, int source_track_id, int dest_track_id, std::optional<int> before_processor_id) = 0;\n    virtual ControlResponse create_processor_on_track(const std::string& name, const std::string& uid, const std::string& file,\n                                                      PluginType type, int track_id, std::optional<int> before_processor_id) = 0;\n\n    virtual ControlResponse delete_processor_from_track(int processor_id, int track_id) = 0;\n    virtual ControlResponse delete_track(int track_id) = 0;\n\nprotected:\n    AudioGraphController() = default;\n};\n\nclass ProgramController\n{\npublic:\n    virtual ~ProgramController() = default;\n\n    [[nodiscard]] virtual std::pair<ControlStatus, int>                      get_processor_current_program(int processor_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::string>              get_processor_current_program_name(int processor_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::string>              get_processor_program_name(int processor_id, int program_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<std::string>> get_processor_programs(int processor_id) const = 0;\n\n    virtual ControlResponse                                       set_processor_program(int processor_id, int program_id)= 0;\n\nprotected:\n    ProgramController() = default;\n};\n\nclass ParameterController\n{\npublic:\n    virtual ~ParameterController() = default;\n\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<ParameterInfo>> get_processor_parameters(int processor_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<ParameterInfo>> get_track_parameters(int processor_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, int>                        get_parameter_id(int processor_id, const std::string& parameter) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, ParameterInfo>              get_parameter_info(int processor_id, int parameter_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, float>                      get_parameter_value(int processor_id, int parameter_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, float>                      get_parameter_value_in_domain(int processor_id, int parameter_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::string>                get_parameter_value_as_string(int processor_id, int parameter_id) const = 0;\n\n    virtual ControlStatus set_parameter_value(int processor_id, int parameter_id, float value) = 0;\n\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<PropertyInfo>>  get_processor_properties(int processor_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<PropertyInfo>>  get_track_properties(int processor_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, int>                        get_property_id(int processor_id, const std::string& parameter) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, PropertyInfo>               get_property_info(int processor_id, int parameter_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::string>                get_property_value(int processor_id, int parameter_id) const = 0;\n\n    virtual ControlStatus set_property_value(int processor_id, int parameter_id, const std::string& value) = 0;\n\nprotected:\n    ParameterController() = default;\n};\n\nclass MidiController\n{\npublic:\n    virtual ~MidiController() = default;\n\n    [[nodiscard]] virtual int                            get_input_ports() const = 0;\n    [[nodiscard]] virtual int                            get_output_ports() const = 0;\n    [[nodiscard]] virtual std::vector<MidiKbdConnection> get_all_kbd_input_connections() const = 0;\n    [[nodiscard]] virtual std::vector<MidiKbdConnection> get_all_kbd_output_connections() const = 0;\n    [[nodiscard]] virtual std::vector<MidiCCConnection>  get_all_cc_input_connections() const = 0;\n    [[nodiscard]] virtual std::vector<MidiPCConnection>  get_all_pc_input_connections() const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<MidiCCConnection>> get_cc_input_connections_for_processor(int processor_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<MidiPCConnection>> get_pc_input_connections_for_processor(int processor_id) const = 0;\n\n    [[nodiscard]] virtual bool get_midi_clock_output_enabled(int port) const = 0;\n    virtual ControlStatus set_midi_clock_output_enabled(bool enabled, int port) = 0;\n\n    virtual ControlResponse connect_kbd_input_to_track(int track_id, MidiChannel channel, int port, bool raw_midi) = 0;\n    virtual ControlResponse connect_kbd_output_from_track(int track_id, MidiChannel channel, int port) = 0;\n    virtual ControlResponse connect_cc_to_parameter(int processor_id, int parameter_id, MidiChannel channel, int port,\n                                                    int cc_number, float min_range, float max_range, bool relative_mode) = 0;\n    virtual ControlResponse connect_pc_to_processor(int processor_id, MidiChannel channel, int port) = 0;\n\n    virtual ControlResponse disconnect_kbd_input(int track_id, MidiChannel channel, int port, bool raw_midi) = 0;\n    virtual ControlResponse disconnect_kbd_output(int track_id, MidiChannel channel, int port) = 0;\n    virtual ControlResponse disconnect_cc(int processor_id, MidiChannel channel, int port, int cc_number) = 0;\n    virtual ControlResponse disconnect_pc(int processor_id, MidiChannel channel, int port) = 0;\n    virtual ControlResponse disconnect_all_cc_from_processor(int processor_id) = 0;\n    virtual ControlResponse disconnect_all_pc_from_processor(int processor_id) = 0;\n\nprotected:\n    MidiController() = default;\n};\n\nclass AudioRoutingController\n{\npublic:\n    virtual ~AudioRoutingController() = default;\n\n    [[nodiscard]] virtual std::vector<AudioConnection> get_all_input_connections() const = 0;\n    [[nodiscard]] virtual std::vector<AudioConnection> get_all_output_connections() const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<AudioConnection>> get_input_connections_for_track(int track_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<AudioConnection>> get_output_connections_for_track(int track_id) const = 0;\n\n    virtual ControlResponse                connect_input_channel_to_track(int track_id, int track_channel, int input_channel) = 0;\n    virtual ControlResponse                connect_output_channel_to_track(int track_id, int track_channel, int output_channel) = 0;\n\n    virtual ControlResponse                disconnect_input(int track_id, int track_channel, int input_channel) = 0;\n    virtual ControlResponse                disconnect_output(int track_id, int track_channel, int output_channel) = 0;\n    virtual ControlResponse                disconnect_all_inputs_from_track(int track_id) = 0;\n    virtual ControlResponse                disconnect_all_outputs_from_track(int track_id) = 0;\n\nprotected:\n    AudioRoutingController() = default;\n};\n\nclass CvGateController\n{\npublic:\n    virtual ~CvGateController() = default;\n\n    [[nodiscard]] virtual int get_cv_input_ports() const = 0;\n    [[nodiscard]] virtual int get_cv_output_ports() const = 0;\n\n\n    [[nodiscard]] virtual std::vector<CvConnection>   get_all_cv_input_connections() const = 0;\n    [[nodiscard]] virtual std::vector<CvConnection>   get_all_cv_output_connections() const = 0;\n    [[nodiscard]] virtual std::vector<GateConnection> get_all_gate_input_connections() const = 0;\n    [[nodiscard]] virtual std::vector<GateConnection> get_all_gate_output_connections() const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<CvConnection>>   get_cv_input_connections_for_processor(int processor_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<CvConnection>>   get_cv_output_connections_for_processor(int processor_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<GateConnection>> get_gate_input_connections_for_processor(int processor_id) const = 0;\n    [[nodiscard]] virtual std::pair<ControlStatus, std::vector<GateConnection>> get_gate_output_connections_for_processor(int processor_id) const = 0;\n\n    virtual ControlResponse               connect_cv_input_to_parameter(int processor_id, int parameter_id, int cv_input_id) = 0;\n    virtual ControlResponse               connect_cv_output_from_parameter(int processor_id, int parameter_id, int cv_output_id) = 0;\n    virtual ControlResponse               connect_gate_input_to_processor(int processor_id, int gate_input_id, int channel, int note_no) = 0;\n    virtual ControlResponse               connect_gate_output_from_processor(int processor_id, int gate_output_id, int channel, int note_no) = 0;\n\n    virtual ControlResponse               disconnect_cv_input(int processor_id, int parameter_id, int cv_input_id) = 0;\n    virtual ControlResponse               disconnect_cv_output(int processor_id, int parameter_id, int cv_output_id) = 0;\n    virtual ControlResponse               disconnect_gate_input(int processor_id, int gate_input_id, int channel, int note_no) = 0;\n    virtual ControlResponse               disconnect_gate_output(int processor_id, int gate_output_id, int channel, int note_no) = 0;\n    virtual ControlResponse               disconnect_all_cv_inputs_from_processor(int processor_id) = 0;\n    virtual ControlResponse               disconnect_all_cv_outputs_from_processor(int processor_id) = 0;\n    virtual ControlResponse               disconnect_all_gate_inputs_from_processor(int processor_id) = 0;\n    virtual ControlResponse               disconnect_all_gate_outputs_from_processor(int processor_id) = 0;\n\nprotected:\n    CvGateController() = default;\n};\n\nclass OscController\n{\npublic:\n    virtual ~OscController() = default;\n\n    [[nodiscard]] virtual std::string get_send_ip() const = 0;\n    [[nodiscard]] virtual int get_send_port() const = 0;\n    [[nodiscard]] virtual int get_receive_port() const = 0;\n    [[nodiscard]] virtual std::vector<std::string> get_enabled_parameter_outputs() const = 0;\n    virtual ControlResponse enable_output_for_parameter(int processor_id, int parameter_id) = 0;\n    virtual ControlResponse disable_output_for_parameter(int processor_id, int parameter_id) = 0;\n    virtual ControlResponse enable_all_output() = 0;\n    virtual ControlResponse disable_all_output() = 0;\n\nprotected:\n    OscController() = default;\n};\n\nclass SessionController\n{\npublic:\n    virtual ~SessionController() = default;\n\n    [[nodiscard]] virtual SessionState save_session() const = 0;\n    virtual ControlResponse restore_session(const SessionState& state) = 0;\n};\n\nclass ControlNotification\n{\npublic:\n    virtual ~ControlNotification() = default;\n\n    [[nodiscard]] NotificationType type() const {return _type;}\n    [[nodiscard]] Time timestamp() const {return _timestamp;}\n\nprotected:\n    ControlNotification(NotificationType type, Time timestamp) : _type(type),\n                                                                 _timestamp(timestamp) {}\n\nprivate:\n    NotificationType _type;\n    Time _timestamp;\n};\n\nclass ControlListener\n{\npublic:\n    virtual void notification(const ControlNotification* notification) = 0;\n};\n\nclass SushiControl\n{\npublic:\n    virtual ~SushiControl() = default;\n\n    SystemController*       system_controller() {return _system_controller;}\n    TransportController*    transport_controller() {return _transport_controller;}\n    TimingController*       timing_controller() {return _timing_controller;}\n    KeyboardController*     keyboard_controller() {return _keyboard_controller;}\n    AudioGraphController*   audio_graph_controller() {return _audio_graph_controller;}\n    ProgramController*      program_controller() {return _program_controller;}\n    ParameterController*    parameter_controller() {return _parameter_controller;}\n    MidiController*         midi_controller() {return _midi_controller;}\n    AudioRoutingController* audio_routing_controller() {return _audio_routing_controller;}\n    CvGateController*       cv_gate_controller() {return _cv_gate_controller;}\n    OscController*          osc_controller() {return _osc_controller;}\n    SessionController*      session_controller() {return _session_controller;}\n\n    virtual ControlStatus   subscribe_to_notifications(NotificationType type, ControlListener* listener) = 0;\n\nprotected:\n    SushiControl(SystemController*       system_controller,\n                 TransportController*    transport_controller,\n                 TimingController*       timing_controller,\n                 KeyboardController*     keyboard_controller,\n                 AudioGraphController*   audio_graph_controller,\n                 ProgramController*      program_controller,\n                 ParameterController*    parameter_controller,\n                 MidiController*         midi_controller,\n                 AudioRoutingController* audio_routing_controller,\n                 CvGateController*       cv_gate_controller,\n                 OscController*          osc_controller,\n                 SessionController*      session_controller) : _system_controller(system_controller),\n                                                               _transport_controller(transport_controller),\n                                                               _timing_controller(timing_controller),\n                                                               _keyboard_controller(keyboard_controller),\n                                                               _audio_graph_controller(audio_graph_controller),\n                                                               _program_controller(program_controller),\n                                                               _parameter_controller(parameter_controller),\n                                                               _midi_controller(midi_controller),\n                                                               _audio_routing_controller(audio_routing_controller),\n                                                               _cv_gate_controller(cv_gate_controller),\n                                                               _osc_controller(osc_controller),\n                                                               _session_controller(session_controller){}\n\nprivate:\n    SystemController*           _system_controller;\n    TransportController*        _transport_controller;\n    TimingController*           _timing_controller;\n    KeyboardController*         _keyboard_controller;\n    AudioGraphController*       _audio_graph_controller;\n    ProgramController*          _program_controller;\n    ParameterController*        _parameter_controller;\n    MidiController*             _midi_controller;\n    AudioRoutingController*     _audio_routing_controller;\n    CvGateController*           _cv_gate_controller;\n    OscController*              _osc_controller;\n    SessionController*          _session_controller;\n};\n\n} // end namespace sushi::control\n\nELK_POP_WARNING\n\n#endif //SUSHI_CONTROL_INTERFACE_H\n"
  },
  {
    "path": "include/sushi/control_notifications.h",
    "content": "\n#ifndef SUSHI_CONTROL_NOTIFICATIONS_H\n#define SUSHI_CONTROL_NOTIFICATIONS_H\n\n#include <variant>\n#include \"control_interface.h\"\n\nnamespace sushi::control {\n\nusing TransportNotificationValue = std::variant<float, PlayingMode, SyncMode, TimeSignature>;\n\nclass TransportNotification : public ControlNotification\n{\npublic:\n    TransportNotification(TransportAction action,\n                          TransportNotificationValue value,\n                          Time timestamp)\n            : ControlNotification(NotificationType::TRANSPORT_UPDATE, timestamp),\n              _value(value),\n              _action(action) {}\n\n    TransportAction action() const {return _action;}\n    TransportNotificationValue value() const {return _value;}\n\nprivate:\n    TransportNotificationValue _value;\n    TransportAction _action;\n};\n\nclass CpuTimingNotification : public ControlNotification\n{\npublic:\n    CpuTimingNotification(const CpuTimings& timings, Time timestamp)\n            : ControlNotification(NotificationType::CPU_TIMING_UPDATE, timestamp),\n              _cpu_timings(timings) {}\n\n    CpuTimingNotification(CpuTimings&& timings, Time timestamp)\n            : ControlNotification(NotificationType::CPU_TIMING_UPDATE, timestamp),\n              _cpu_timings(std::move(timings)) {}\n\n    const CpuTimings& cpu_timings() const {return _cpu_timings;}\n\nprivate:\n    CpuTimings _cpu_timings;\n};\n\nclass TrackNotification : public ControlNotification\n{\npublic:\n    TrackNotification(TrackAction action, int track_id, Time timestamp)\n            : ControlNotification(NotificationType::TRACK_UPDATE, timestamp),\n              _track_id(track_id),\n              _action(action) {}\n\n    int track_id() const {return _track_id;}\n    TrackAction action() const {return _action;}\n\nprivate:\n    int _track_id;\n    TrackAction _action;\n};\n\nclass ProcessorNotification : public ControlNotification\n{\npublic:\n    ProcessorNotification(ProcessorAction action, int processor_id, int parent_track_id, Time timestamp)\n            : ControlNotification(NotificationType::PROCESSOR_UPDATE, timestamp),\n              _processor_id(processor_id),\n              _parent_track_id(parent_track_id),\n              _action(action) {}\n\n    int processor_id() const {return _processor_id;}\n    int parent_track_id() const {return _parent_track_id;}\n    ProcessorAction action() const {return _action;}\n\nprivate:\n    int _processor_id;\n    int _parent_track_id;\n    ProcessorAction _action;\n};\n\nclass ParameterChangeNotification : public ControlNotification\n{\npublic:\n    ParameterChangeNotification(int processor_id, int parameter_id, float normalized_value,\n                                float domain_value, const std::string& formatted_value, Time timestamp)\n    : ControlNotification(NotificationType::PARAMETER_CHANGE, timestamp),\n      _processor_id(processor_id),\n      _parameter_id(parameter_id),\n      _normalized_value(normalized_value),\n      _domain_value(domain_value),\n      _formatted_value(formatted_value) {}\n\n    int processor_id() const {return _processor_id;}\n    int parameter_id() const {return _parameter_id;}\n    float value() const {return _normalized_value;}\n    float domain_value() const {return _domain_value;}\n    const std::string& formatted_value() const {return _formatted_value;};\n\nprivate:\n    int _processor_id;\n    int _parameter_id;\n    float _normalized_value;\n    float _domain_value;\n    std::string _formatted_value;\n};\n\nclass PropertyChangeNotification : public ControlNotification\n{\npublic:\n    PropertyChangeNotification(int processor_id, int property_id, const std::string& value, Time timestamp)\n    : ControlNotification(NotificationType::PROPERTY_CHANGE, timestamp),\n      _processor_id(processor_id),\n      _property_id(property_id),\n      _value(value) {}\n\n    PropertyChangeNotification(int processor_id, int property_id, std::string&& value, Time timestamp)\n    : ControlNotification(NotificationType::PROPERTY_CHANGE, timestamp),\n      _processor_id(processor_id),\n      _property_id(property_id),\n      _value(value) {}\n\n    int processor_id() const {return _processor_id;}\n    int parameter_id() const {return _property_id;}\n    const std::string& value() const {return _value;}\n\nprivate:\n    int _processor_id;\n    int _property_id;\n    std::string _value;\n};\n\nclass CommandCompletionNotification : public ControlNotification\n{\npublic:\n    CommandCompletionNotification(ControlStatus status, int id, Time timestamp)\n    : ControlNotification(NotificationType::ASYNC_COMMAND_COMPLETION, timestamp), _status(status), _id(id) {}\n\n    ControlStatus status() const {return _status;}\n    int id() const {return _id;}\n\nprivate:\n    ControlStatus _status;\n    int           _id;\n};\n\n} // end namespace sushi::control\n\n#endif //SUSHI_CONTROL_NOTIFICATIONS_H"
  },
  {
    "path": "include/sushi/coreaudio_devices_dump.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utility functions for dumping CoreAudio devices info\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"rapidjson/document.h\"\n\nnamespace sushi {\n\n/**\n * @brief Retrieve CoreAudio's registered devices information.\n *        Can be queried before instantiating an actual CoreAudioFrontend\n *\n * @return Device information list in JSON format\n */\nrapidjson::Document generate_coreaudio_devices_info_document();\n\n} // end namespace sushi\n\n"
  },
  {
    "path": "include/sushi/elk_sentry_log_sink.h",
    "content": "/**\n * @brief An spdlog sink which wraps the Sentry logging functionality\n * @Copyright 2017-2024 Elk Audio AB, Stockholm\n */\n\n#ifndef ELK_SENTRY_LOG_SINK_H\n#define ELK_SENTRY_LOG_SINK_H\n\n#include <iostream>\n#include <mutex>\n\n#include \"spdlog/details/null_mutex.h\"\n#include \"spdlog/sinks/base_sink.h\"\n#include \"sentry.h\"\n\nnamespace elk {\n\ntemplate<typename Mutex>\nclass SentrySink : public spdlog::sinks::base_sink<Mutex>\n{\npublic:\n    SentrySink() : spdlog::sinks::base_sink<Mutex>()\n    {\n    }\n\n    ~SentrySink()\n    {\n    }\n\nprotected:\n    void sink_it_(const spdlog::details::log_msg& msg) override\n    {\n        const std::string payload(msg.payload.begin(), msg.payload.end());\n        const std::string logger_name(msg.logger_name.begin(), msg.logger_name.end());\n        switch (msg.level)\n        {\n        case spdlog::level::info: _add_breadcrumb(payload, logger_name, \"info\"); break;\n        case spdlog::level::debug: _add_breadcrumb(payload, logger_name, \"debug\"); break;\n        case spdlog::level::warn: _add_breadcrumb(payload, logger_name, \"warning\"); break;\n        case spdlog::level::err:\n            sentry_capture_event(sentry_value_new_message_event(\n            SENTRY_LEVEL_ERROR, // level\n            logger_name.c_str(), // logger name\n            payload.c_str() // message\n            ));\n            break;\n\n        default:\n            break;\n        }\n    }\n\n    void flush_() override\n    {\n       sentry_flush(1000);\n    }\n\nprivate:\n    void _add_breadcrumb(const std::string& message, const std::string& category, const std::string& level)\n    {\n        sentry_value_t crumb = sentry_value_new_breadcrumb(\"log\", message.c_str());\n        sentry_value_set_by_key(crumb, \"category\", sentry_value_new_string(category.c_str()));\n        sentry_value_set_by_key(crumb, \"level\", sentry_value_new_string(level.c_str()));\n        sentry_add_breadcrumb(crumb);\n    }\n};\n\nusing sentry_sink_mt = SentrySink<std::mutex>;\nusing sentry_sink_st = SentrySink<spdlog::details::null_mutex>;\n\n}\n\n#endif // SUSHI_SENTRY_LOG_SINK_H"
  },
  {
    "path": "include/sushi/factory_interface.h",
    "content": "/*\n* Copyright 2017-2022 Modern Ancient Instruments Networked AB, dba Elk\n*\n* SUSHI is free software: you can redistribute it and/or modify it under the terms of\n* the GNU Affero General Public License as published by the Free Software Foundation,\n* either version 3 of the License, or (at your option) any later version.\n*\n* SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n* PURPOSE. See the GNU Affero General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License along with\n* SUSHI. If not, see http://www.gnu.org/licenses/\n*/\n\n#ifndef FACTORY_INTERFACE_H\n#define FACTORY_INTERFACE_H\n\n#include \"sushi.h\"\n\nnamespace sushi {\n\nclass FactoryInterface\n{\npublic:\n    FactoryInterface() = default;\n    virtual ~FactoryInterface() = default;\n\n    [[nodiscard]] virtual std::pair<std::unique_ptr<Sushi>, Status> new_instance(SushiOptions& options) = 0;\n};\n\n} // end namespace sushi\n\n#endif // FACTORY_INTERFACE_H\n"
  },
  {
    "path": "include/sushi/offline_factory.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Public Sushi factory for offline use.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef OFFLINE_FACTORY_H\n#define OFFLINE_FACTORY_H\n\n#include \"factory_interface.h\"\n#include \"sushi.h\"\n\nnamespace sushi {\n\nnamespace internal {\nclass OfflineFactoryImplementation;\n}\n\nclass OfflineFactory : public FactoryInterface\n{\npublic:\n    OfflineFactory();\n    ~OfflineFactory() override;\n\n    [[nodiscard]] std::pair<std::unique_ptr<Sushi>, Status> new_instance(SushiOptions& options) override;\n\nprivate:\n    std::unique_ptr<internal::OfflineFactoryImplementation> _implementation;\n};\n\n} // end namespace sushi\n\n#endif // STANDALONE_FACTORY_H\n"
  },
  {
    "path": "include/sushi/options.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Option parsing\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_OPTIONS_H\n#define SUSHI_OPTIONS_H\n\n#include <cstdio>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_ZERO_AS_NULL_POINTER_CONSTANT\nELK_DISABLE_SIGN_CONVERSION\nELK_DISABLE_CONDITIONAL_UNINITIALIZED\n#include \"optionparser.h\"\nELK_POP_WARNING\n\n#define _SUSHI_STRINGIZE(X) #X\n#define SUSHI_STRINGIZE(X) _SUSHI_STRINGIZE(X)\n\n////////////////////////////////////////////////////////////////////////////////\n// Options Defaults\n////////////////////////////////////////////////////////////////////////////////\n\n#define ELKLOG_LOG_LEVEL_DEFAULT \"info\"\n#define ELKLOG_LOG_FILE_DEFAULT \"/tmp/sushi.log\"\n#define SUSHI_JSON_FILENAME_DEFAULT \"config.json\"\n#define SUSHI_JSON_STRING_DEFAULT \"{}\"\n#define SUSHI_SAMPLE_RATE_DEFAULT 48000\n#define SUSHI_JACK_CLIENT_NAME_DEFAULT \"sushi\"\n#define SUSHI_OSC_SERVER_PORT_DEFAULT 24024\n#define SUSHI_OSC_SEND_PORT_DEFAULT 24023\n#define SUSHI_OSC_SEND_IP_DEFAULT \"127.0.0.1\"\n#if defined(_MSC_VER)\n    #define SUSHI_GRPC_LISTENING_PORT_DEFAULT \"[::]:510\"\n#else\n    #define SUSHI_GRPC_LISTENING_PORT_DEFAULT \"[::]:51051\"\n#endif\n#define SUSHI_PORTAUDIO_INPUT_LATENCY_DEFAULT 0.0f\n#define SUSHI_PORTAUDIO_OUTPUT_LATENCY_DEFAULT 0.0f\n#define SUSHI_SENTRY_CRASH_HANDLER_PATH_DEFAULT \"./crashpad_handler\"\n#ifdef SUSHI_BUILD_WITH_SENTRY\n    #define SUSHI_SENTRY_DSN_DEFAULT SUSHI_SENTRY_DSN\n#else\n    #define SUSHI_SENTRY_DSN_DEFAULT \"\"\n#endif\n\nnamespace sushi {\n\n////////////////////////////////////////////////////////////////////////////////\n// Helpers for optionparse\n////////////////////////////////////////////////////////////////////////////////\n\nstruct SushiArg : public optionparser::Arg\n{\n    static void print_error(const char* msg1, const optionparser::Option& opt, const char* msg2)\n    {\n        fprintf(stderr, \"%s\", msg1);\n        fwrite(opt.name, static_cast<size_t>(opt.namelen), 1, stderr);\n        fprintf(stderr, \"%s\", msg2);\n    }\n\n    static optionparser::ArgStatus Unknown(const optionparser::Option& option, bool msg)\n    {\n        if (msg)\n        {\n            print_error(\"Unknown option '\", option, \"'\\n\");\n        }\n        return optionparser::ARG_ILLEGAL;\n    }\n\n    static optionparser::ArgStatus NonEmpty(const optionparser::Option& option, bool msg)\n    {\n        if (option.arg && option.arg[0])\n        {\n            return optionparser::ARG_OK;\n        }\n\n        if (msg)\n        {\n            print_error(\"Option '\", option, \"' requires a non-empty argument\\n\");\n        }\n        return optionparser::ARG_ILLEGAL;\n    }\n\n    static optionparser::ArgStatus Numeric(const optionparser::Option& option, bool msg)\n    {\n        char* endptr = nullptr;\n        if (option.arg != nullptr && strtol(option.arg, &endptr, 10))\n        {}\n\n        if (endptr != option.arg && *endptr == 0)\n        {\n          return optionparser::ARG_OK;\n        }\n        if (msg)\n        {\n            print_error(\"Option '\", option, \"' requires a numeric argument\\n\");\n        }\n        return optionparser::ARG_ILLEGAL;\n    }\n};\n\n////////////////////////////////////////////////////////////////////////////////\n// Command Line options descriptors\n////////////////////////////////////////////////////////////////////////////////\n\n// List here the different command line options\nenum OptionIndex\n{\n    OPT_IDX_UNKNOWN,\n    OPT_IDX_HELP,\n    OPT_IDX_VERSION,\n    OPT_IDX_LOG_LEVEL,\n    OPT_IDX_LOG_FILE,\n    OPT_IDX_LOG_FLUSH_INTERVAL,\n    OPT_IDX_DUMP_PARAMETERS,\n    OPT_IDX_CONFIG_FILE,\n    OPT_IDX_USE_OFFLINE,\n    OPT_IDX_INPUT_FILE,\n    OPT_IDX_OUTPUT_FILE,\n    OPT_IDX_USE_DUMMY,\n    OPT_IDX_USE_PORTAUDIO,\n    OPT_IDX_USE_APPLE_COREAUDIO,\n    OPT_IDX_AUDIO_INPUT_DEVICE,\n    OPT_IDX_AUDIO_INPUT_DEVICE_UID,\n    OPT_IDX_AUDIO_OUTPUT_DEVICE,\n    OPT_IDX_AUDIO_OUTPUT_DEVICE_UID,\n    OPT_IDX_PA_SUGGESTED_INPUT_LATENCY,\n    OPT_IDX_PA_SUGGESTED_OUTPUT_LATENCY,\n    OPT_IDX_DUMP_DEVICES,\n    OPT_IDX_USE_JACK,\n    OPT_IDX_CONNECT_PORTS,\n    OPT_IDX_JACK_CLIENT,\n    OPT_IDX_JACK_SERVER,\n    OPT_IDX_USE_XENOMAI_RASPA,\n    OPT_IDX_XENOMAI_DEBUG_MODE_SW,\n    OPT_IDX_MULTICORE_PROCESSING,\n    OPT_IDX_TIMINGS_STATISTICS,\n    OPT_IDX_DETAILED_TIMINGS,\n    OPT_IDX_OSC_RECEIVE_PORT,\n    OPT_IDX_OSC_SEND_PORT,\n    OPT_IDX_OSC_SEND_IP,\n    OPT_IDX_GRPC_LISTEN_ADDRESS,\n    OPT_IDX_NO_OSC,\n    OPT_IDX_NO_GRPC,\n    OPT_IDX_BASE_PLUGIN_PATH,\n    OPT_IDX_SENTRY_CRASH_HANDLER,\n    OPT_IDX_SENTRY_DSN\n};\n\n// Option types (UNUSED is generally used for options that take a value as argument)\nenum OptionType\n{\n    OPT_TYPE_UNUSED = 0,\n    OPT_TYPE_DISABLED = 1,\n    OPT_TYPE_ENABLED = 2\n};\n\n// Option descriptors, one for each entry of the OptionIndex enum\nconst optionparser::Descriptor usage[] =\n{\n    {\n        OPT_IDX_UNKNOWN,    // index\n        OPT_TYPE_UNUSED,    // type\n        \"\",         // shortopt\n        \"\",         // longopt\n        SushiArg::Unknown, // check_arg\n        \"\\nUSAGE: sushi -r|-j|-o|-d [options] \\n\\nOptions:\" // help\n    },\n    {\n        OPT_IDX_HELP,\n        OPT_TYPE_UNUSED,\n        \"h?\",\n        \"help\",\n        SushiArg::None,\n        \"\\t\\t-h --help \\tPrint usage and exit.\"\n    },\n    {\n        OPT_IDX_VERSION,\n        OPT_TYPE_UNUSED,\n        \"v\",\n        \"version\",\n        SushiArg::None,\n        \"\\t\\t-v --version \\tPrint version information and exit.\"\n    },\n    {\n        OPT_IDX_LOG_LEVEL,\n        OPT_TYPE_UNUSED,\n        \"l\",\n        \"log-level\",\n        SushiArg::NonEmpty,\n        \"\\t\\t-l <level>, --log-level=<level> \\tSpecify minimum logging level, from ('debug', 'info', 'warning', 'error') [default=\" ELKLOG_LOG_LEVEL_DEFAULT \"].\"\n    },\n    {\n        OPT_IDX_LOG_FILE,\n        OPT_TYPE_UNUSED,\n        \"L\",\n        \"log-file\",\n        SushiArg::NonEmpty,\n        \"\\t\\t-L <filename>, --log-file=<filename> \\tSpecify logging file destination [default=\" ELKLOG_LOG_FILE_DEFAULT\n      \"].\"\n    },\n    {\n        OPT_IDX_LOG_FLUSH_INTERVAL,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"log-flush-interval\",\n        SushiArg::NonEmpty,\n        \"\\t\\t--log-flush-interval=<seconds> \\tEnable flushing the log periodically and specify the interval.\"\n    },\n    {\n        OPT_IDX_DUMP_PARAMETERS,\n        OPT_TYPE_DISABLED,\n        \"\",\n        \"dump-plugins\",\n        SushiArg::Optional,\n        \"\\t\\t--dump-plugins \\tDump plugin and parameter data to stdout in JSON format.\"\n    },\n    {\n        OPT_IDX_CONFIG_FILE,\n        OPT_TYPE_UNUSED,\n        \"c\",\n        \"config-file\",\n        SushiArg::NonEmpty,\n        \"\\t\\t-c <filename>, --config-file=<filename> \\tSpecify configuration JSON file [default=\" SUSHI_JSON_FILENAME_DEFAULT \"].\"\n    },\n    {\n        OPT_IDX_USE_OFFLINE,\n        OPT_TYPE_DISABLED,\n        \"o\",\n        \"offline\",\n        SushiArg::Optional,\n        \"\\t\\t-o --offline \\tUse offline file audio frontend.\"\n    },\n    {\n        OPT_IDX_INPUT_FILE,\n        OPT_TYPE_UNUSED,\n        \"i\",\n        \"input\",\n        SushiArg::NonEmpty,\n        \"\\t\\t-i <filename>, --input=<filename> \\tSpecify input file, required for --offline option.\"\n    },\n    {\n        OPT_IDX_OUTPUT_FILE,\n        OPT_TYPE_UNUSED,\n        \"O\",\n        \"output\",\n        SushiArg::NonEmpty,\n        \"\\t\\t-O <filename>, --output=<filename> \\tSpecify output file [default= (input_file).proc.wav].\"\n    },\n    {\n        OPT_IDX_USE_DUMMY,\n        OPT_TYPE_DISABLED,\n        \"d\",\n        \"dummy\",\n        SushiArg::Optional,\n        \"\\t\\t-d --dummy \\tUse dummy audio frontend. Useful for debugging.\"\n    },\n    {\n        OPT_IDX_USE_PORTAUDIO,\n        OPT_TYPE_DISABLED,\n        \"a\",\n        \"portaudio\",\n        SushiArg::Optional,\n        \"\\t\\t-a --portaudio \\tUse PortAudio realtime audio frontend.\"\n    },\n    {\n        OPT_IDX_USE_APPLE_COREAUDIO,\n        OPT_TYPE_DISABLED,\n        \"\",\n        \"coreaudio\",\n        SushiArg::Optional,\n        \"\\t\\t--coreaudio \\tUse Apple CoreAudio realtime audio frontend.\"\n    },\n    {\n        OPT_IDX_AUDIO_INPUT_DEVICE,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"audio-input-device\",\n        SushiArg::Optional,\n        \"\\t\\t--audio-input-device=<device id> \\tIndex of the device to use for audio input with portaudio frontend [default=system default]\"\n    },\n    {\n        OPT_IDX_AUDIO_OUTPUT_DEVICE,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"audio-output-device\",\n        SushiArg::Optional,\n        \"\\t\\t--audio-output-device=<device id> \\tIndex of the device to use for audio output with portaudio frontend [default=system default]\"\n    },\n    {\n        OPT_IDX_AUDIO_INPUT_DEVICE_UID,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"audio-input-device-uid\",\n        SushiArg::Optional,\n        \"\\t\\t--audio-input-device-uid=<device uid> \\tUID of the device to use for audio input with Apple CoreAudio frontend [default=system default]\"\n    },\n    {\n        OPT_IDX_AUDIO_OUTPUT_DEVICE_UID,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"audio-output-device-uid\",\n        SushiArg::Optional,\n        \"\\t\\t--audio-output-device-uid=<device uid> \\tUID of the device to use for audio output with Apple CoreAudio frontend [default=system default]\"\n    },\n    {\n        OPT_IDX_PA_SUGGESTED_INPUT_LATENCY,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"pa-suggested-input-latency\",\n        SushiArg::Optional,\n        \"\\t\\t--pa-suggested-input-latency=<latency> \\tInput latency in seconds to suggest to portaudio. Will be rounded up to closest available latency depending on audio API [default=0.0]\"\n    },\n    {\n        OPT_IDX_PA_SUGGESTED_OUTPUT_LATENCY,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"pa-suggested-output-latency\",\n        SushiArg::Optional,\n        \"\\t\\t--pa-suggested-output-latency=<latency> \\tOutput latency in seconds to suggest to portaudio. Will be rounded up to closest available latency depending on audio API [default=0.0]\"\n    },\n    {\n        OPT_IDX_DUMP_DEVICES,\n        OPT_TYPE_DISABLED,\n        \"\",\n        \"dump-audio-devices\",\n        SushiArg::Optional,\n        \"\\t\\t--dump-audio-devices \\tDump available audio devices to stdout in JSON format. Requires a frontend to be specified.\"\n    },\n    {\n        OPT_IDX_USE_JACK,\n        OPT_TYPE_DISABLED,\n        \"j\",\n        \"jack\",\n        SushiArg::Optional,\n        \"\\t\\t-j --jack \\tUse Jack realtime audio frontend.\"\n    },\n    {\n        OPT_IDX_CONNECT_PORTS,\n        OPT_TYPE_DISABLED,\n        \"\",\n        \"connect-ports\",\n        SushiArg::Optional,\n        \"\\t\\t--connect-ports \\tTry to automatically connect Jack ports at startup.\"\n    },\n    {\n        OPT_IDX_JACK_CLIENT,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"client-name\",\n        SushiArg::NonEmpty,\n        \"\\t\\t--client-name=<jack client name> \\tSpecify name of Jack client [default=sushi].\"\n    },\n    {\n        OPT_IDX_JACK_SERVER,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"server-name\",\n        SushiArg::NonEmpty,\n        \"\\t\\t--server-name=<jack server name> \\tSpecify name of Jack server to connect to [determined by jack if empty].\"\n    },\n    {\n        OPT_IDX_USE_XENOMAI_RASPA,\n        OPT_TYPE_DISABLED,\n        \"r\",\n        \"raspa\",\n        SushiArg::Optional,\n        \"\\t\\t-r --raspa \\tUse Xenomai real-time frontend with RASPA driver.\"\n    },\n    {\n        OPT_IDX_XENOMAI_DEBUG_MODE_SW,\n        OPT_TYPE_DISABLED,\n        \"\",\n        \"debug-mode-sw\",\n        SushiArg::Optional,\n        \"\\t\\t--debug-mode-sw \\tBreak to debugger if a mode switch is detected (Xenomai only).\"\n    },\n    {\n        OPT_IDX_MULTICORE_PROCESSING,\n        OPT_TYPE_UNUSED,\n        \"m\",\n        \"multicore-processing\",\n        SushiArg::Numeric,\n        \"\\t\\t-m <n>, --multicore-processing=<n> \\tProcess audio multithreaded with n cores [default n=1 (off)].\"\n    },\n    {\n        OPT_IDX_TIMINGS_STATISTICS,\n        OPT_TYPE_DISABLED,\n        \"\",\n        \"timing-statistics\",\n        SushiArg::Optional,\n        \"\\t\\t--timing-statistics \\tEnable performance timings on all audio processors.\"\n    },\n    {\n        OPT_IDX_DETAILED_TIMINGS,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"detailed-timings\",\n        SushiArg::NonEmpty,\n        \"\\t\\t--detailed-timings \\tRecord timings for all audio callbacks for the given processor name. Use only for debug purposes as it will record significant amounts of data. Multiple processor names may be passed separated by semicolon\"\n    },\n    {\n        OPT_IDX_OSC_RECEIVE_PORT,\n        OPT_TYPE_UNUSED,\n        \"p\",\n        \"osc-rcv-port\",\n        SushiArg::NonEmpty,\n        \"\\t\\t-p <port> --osc-rcv-port=<port> \\tPort to listen for OSC messages on [default port=\" SUSHI_STRINGIZE(\n         SUSHI_OSC_SERVER_PORT_DEFAULT) \"].\"\n    },\n    {\n        OPT_IDX_OSC_SEND_PORT,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"osc-send-port\",\n        SushiArg::NonEmpty,\n        \"\\t\\t--osc-send-port=<port> \\tPort to output OSC messages to [default port=\" SUSHI_STRINGIZE(\n         SUSHI_OSC_SEND_PORT_DEFAULT) \"].\"\n    },\n    {\n        OPT_IDX_OSC_SEND_IP,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"osc-send-ip\",\n        SushiArg::NonEmpty,\n        \"\\t\\t--osc-send-ip=<ip> \\tIP to output OSC messages to [default port=\" SUSHI_STRINGIZE(\n         SUSHI_OSC_SEND_IP_DEFAULT) \"].\"\n    },\n    {\n        OPT_IDX_GRPC_LISTEN_ADDRESS,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"grpc-address\",\n        SushiArg::NonEmpty,\n        \"\\t\\t--grpc-address=<port> \\tgRPC listening address in the format: address:port. By default accepts incoming connections from all ip:s [default port=\" SUSHI_GRPC_LISTENING_PORT_DEFAULT \"].\"\n    },\n    {\n        OPT_IDX_NO_OSC,\n        OPT_TYPE_DISABLED,\n        \"\",\n        \"no-osc\",\n        SushiArg::Optional,\n        \"\\t\\t--no-osc \\tDisable Open Sound Control completely\"\n    },\n    {\n        OPT_IDX_NO_GRPC,\n        OPT_TYPE_DISABLED,\n        \"\",\n        \"no-grpc\",\n        SushiArg::Optional,\n        \"\\t\\t--no-grpc \\tDisable gRPC Control completely\"\n    },\n    {\n        OPT_IDX_BASE_PLUGIN_PATH,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"base-plugin-path\",\n        SushiArg::NonEmpty,\n        \"\\t\\t--base-plugin-path=<path> \\tSpecify a directory to be the base of plugin paths used in JSON / gRPC.\"\n    },\n    {\n        OPT_IDX_SENTRY_CRASH_HANDLER,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"sentry-crash-handler\",\n        SushiArg::NonEmpty,\n        \"\\t\\t--sentry-crash-handler=<path/to/crash_handler> \\tSet the path to the crash handler to use for sentry reports [default path=\" SUSHI_STRINGIZE(SUSHI_SENTRY_CRASH_HANDLER_PATH_DEFAULT) \"].\"\n    },\n    {\n        OPT_IDX_SENTRY_DSN,\n        OPT_TYPE_UNUSED,\n        \"\",\n        \"sentry-dsn\",\n        SushiArg::NonEmpty,\n        \"\\t\\t--sentry-dsn=<dsn.address> \\tSet the DSN that sentry should upload crash logs to [default address=\" SUSHI_STRINGIZE(SUSHI_SENTRY_DSN_DEFAULT) \"].\"\n    },\n    // Don't touch this one (sets default values for optionparser library)\n    { 0, 0, nullptr, nullptr, nullptr, nullptr}\n};\n\n} // end namespace sushi\n\n#endif // SUSHI_OPTIONS_H\n"
  },
  {
    "path": "include/sushi/parameter_dump.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utility functions for writing parameter names to a file.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_PARAMETER_DUMP_H\n#define SUSHI_PARAMETER_DUMP_H\n\n#include \"rapidjson/document.h\"\n\nnamespace sushi {\n\nnamespace control {\n    class SushiControl;\n}\n\nrapidjson::Document generate_processor_parameter_document(sushi::control::SushiControl* engine_controller);\n\n} // end namespace sushi\n\n\n#endif //SUSHI_PARAMETER_DUMP_H\n"
  },
  {
    "path": "include/sushi/portaudio_devices_dump.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utility functions for dumping Portaudio devices info\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"rapidjson/document.h\"\n\nnamespace sushi {\n\n/**\n * @brief Retrieve Portaudio's registered devices information.\n *        Can be queried before instantiating an actual PortaudioFrontend\n *\n * @return Device information list in JSON format\n */\nrapidjson::Document generate_portaudio_devices_info_document();\n\n} // end namespace sushi\n\n"
  },
  {
    "path": "include/sushi/reactive_factory.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Public Sushi factory for reactive use.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef REACTIVE_FACTORY_H\n#define REACTIVE_FACTORY_H\n\n#include \"factory_interface.h\"\n#include \"sushi.h\"\n\nnamespace sushi {\n\nnamespace internal {\n    class ReactiveFactoryImplementation;\n}\n\nclass RtController;\n\nclass ReactiveFactory : public FactoryInterface\n{\npublic:\n    ReactiveFactory();\n    ~ReactiveFactory() override;\n\n    [[nodiscard]] std::pair<std::unique_ptr<Sushi>, Status> new_instance(SushiOptions& options) override;\n\n    /**\n     * @brief Returns an instance of a RealTimeController, if new_instance(...) completed successfully.\n     *        If not, it returns an empty unique_ptr.\n     * @return A unique_ptr with a RtController sub-class, or not, depending on InitStatus.\n     */\n    [[nodiscard]] std::unique_ptr<RtController> rt_controller();\n\nprivate:\n    std::unique_ptr<internal::ReactiveFactoryImplementation> _implementation;\n};\n\n} // end namespace sushi\n\n#endif // REACTIVE_FACTORY_H\n"
  },
  {
    "path": "include/sushi/rt_controller.h",
    "content": "/*\n* Copyright 2017-2022 Modern Ancient Instruments Networked AB, dba Elk\n*\n* SUSHI is free software: you can redistribute it and/or modify it under the terms of\n* the GNU Affero General Public License as published by the Free Software Foundation,\n* either version 3 of the License, or (at your option) any later version.\n*\n* SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n* PURPOSE. See the GNU Affero General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License along with\n* SUSHI. If not, see http://www.gnu.org/licenses/\n*/\n\n#ifndef REAL_TIME_CONTROLLER_H\n#define REAL_TIME_CONTROLLER_H\n\n#include <functional>\n\n#include \"control_interface.h\"\n#include \"sushi_time.h\"\n#include \"types.h\"\n#include \"sample_buffer.h\"\n\nnamespace sushi {\n\nenum class TransportPositionSource\n{\n    EXTERNAL,\n    CALCULATED\n};\n\nusing ReactiveMidiCallback = std::function<void(int output, MidiDataByte data, Time timestamp)>;\n\n/**\n * @brief The API for the methods which can safely be called from a real-time context to interact with Sushi as a library.\n */\nclass RtController\n{\npublic:\n    RtController() = default;\n    virtual ~RtController() = default;\n\n    /// For Transport:\n    /////////////////////////////////////////////////////////////\n\n    /**\n     * @brief Set the tempo of the Sushi transport.\n     *        (can be called from a real-time context).\n     * @param tempo\n     */\n    virtual void set_tempo(float tempo) = 0;\n\n    /**\n     * @brief Set the time signature of the Sushi transport.\n     *        (can be called from a real-time context).\n     * @param time_signature\n     */\n    virtual void set_time_signature(control::TimeSignature time_signature) = 0;\n\n    /**\n     * @brief Set the PlayingMode of the Sushi transport.\n     *        (can be called from a real-time context).\n     * @param mode\n     */\n    virtual void set_playing_mode(control::PlayingMode mode) = 0;\n\n    /**\n     * @brief Set the beat time of the Sushi transport.\n     *        (can be called from a real-time context).\n     * @param beat_time\n     * @return true if the beat time was set, false if PositionSource is not set to EXTERNAL\n     */\n    virtual bool set_current_beats(double beat_time) = 0;\n\n    /**\n     * @brief Set the bar beat count of the Sushi transport.\n     *        (can be called from a real-time context).\n     * @param bar_beat_count\n     * @return true if the bar beat time was set, false if PositionSource is not set to EXTERNAL\n     */\n    virtual bool set_current_bar_beats(double bar_beat_count) = 0;\n\n    /**\n     * @brief Sets which source to use for the beat count position: the internally calculated one, or the one set\n     *        using the set_current_beats method below.\n     * @param TransportPositionSource Enum, EXTERNAL / CALCULATED\n     */\n    virtual void set_position_source(TransportPositionSource ps) = 0;\n\n    /// For Audio:\n    /////////////////////////////////////////////////////////////\n\n    /**\n     * @brief Method to invoke from the host's audio callback.\n     * @param in_buffer Input sample buffer\n     * @param out_buffer Output sample buffer\n     * @param timestamp timestamp for call\n     */\n\n    virtual void process_audio(ChunkSampleBuffer& in_buffer,\n                               ChunkSampleBuffer& out_buffer,\n                               Time timestamp) = 0;\n\n    /**\n     * @brief Call before the first call to process_audio() when resuming from an interrupt or xrun to\n     *        notify sushi that audio processing was interrupted and that there may be gaps in the audio\n     * @param duration The length of the interruption\n     */\n    virtual void notify_interrupted_audio(Time duration) = 0;\n\n    /// For MIDI:\n    /////////////////////////////////////////////////////////////\n\n    /**\n     * @brief Call to pass MIDI input to Sushi\n     * @param input Currently assumed to always be 0 since the frontend only supports a single input device.\n     * @param data MidiDataByte\n     * @param timestamp Sushi Time timestamp for message\n     */\n    virtual void receive_midi(int input, MidiDataByte data, Time timestamp) = 0;\n\n    /**\n     * @brief Assign a callback which is invoked when a MIDI message is generated from inside Sushi.\n     *        (Not safe to call from a real-time context, and should only really be called once).\n     * @param callback\n     */\n    virtual void set_midi_callback(ReactiveMidiCallback&& callback) = 0;\n\n    /**\n     * @brief If the host doesn't provide a timestamp, this method can be used to calculate it,\n     *        based on the sample count from session start.\n     * @return The currently calculated Timestamp.\n     */\n    virtual sushi::Time calculate_timestamp_from_start(float sample_rate) const = 0;\n\n    /**\n     * @brief Call this at the end of each ProcessBlock, to update the sample count and timestamp used for\n     *        time and sample offset calculations.\n     * @param sample_count\n     * @param timestamp\n     */\n    virtual void increment_samples_since_start(int64_t sample_count, Time timestamp) = 0;\n};\n\n} // end namespace sushi\n\n#endif //REAL_TIME_CONTROLLER_H\n"
  },
  {
    "path": "include/sushi/sample_buffer.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief General purpose multichannel audio buffer class\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SAMPLEBUFFER_H\n#define SUSHI_SAMPLEBUFFER_H\n\n#include <algorithm>\n#include <cassert>\n#include <cmath>\n\n#include \"constants.h\"\n\nnamespace sushi {\n\nconstexpr int LEFT_CHANNEL_INDEX = 0;\nconstexpr int RIGHT_CHANNEL_INDEX = 1;\n\ntemplate<int size>\nclass SampleBuffer;\n\ntemplate<int size>\nvoid swap(SampleBuffer<size>& lhs, SampleBuffer<size>& rhs)\n{\n    std::swap(lhs._channel_count, rhs._channel_count);\n    std::swap(lhs._own_buffer, rhs._own_buffer);\n    std::swap(lhs._buffer, rhs._buffer);\n}\n\ntemplate<int size>\nclass SampleBuffer\n{\npublic:\n    /**\n     * @brief Construct a zeroed buffer with specified number of channels\n     */\n    explicit SampleBuffer(int channel_count) : _channel_count(channel_count),\n                                               _own_buffer(true),\n                                               _buffer(new float[static_cast<uint64_t>(size * channel_count)])\n    {\n        clear();\n    }\n\n    /**\n     * @brief Construct an empty buffer object with 0 channels.\n     */\n    SampleBuffer() noexcept : _channel_count(0),\n                              _own_buffer(true),\n                              _buffer(nullptr)\n    {}\n\n    /**\n     * @brief Copy constructor.\n     */\n    SampleBuffer(const SampleBuffer &o) : _channel_count(o._channel_count),\n                                          _own_buffer(o._own_buffer)\n    {\n        if (o._own_buffer)\n        {\n            _buffer = new float[size * o._channel_count];\n            std::copy(o._buffer, o._buffer + (size * o._channel_count), _buffer);\n        }\n        else\n        {\n            _buffer = o._buffer;\n        }\n    }\n\n    /**\n     * @brief Move constructor.\n     */\n    SampleBuffer(SampleBuffer &&o) noexcept : _channel_count(o._channel_count),\n                                              _own_buffer(o._own_buffer),\n                                              _buffer(o._buffer)\n    {\n        o._buffer = nullptr;\n    }\n\n    /**\n     * @brief Destroy the buffer.\n     */\n    ~SampleBuffer()\n    {\n        if (_own_buffer)\n        {\n            delete[] _buffer;\n        }\n    }\n\n    /**\n     * @brief Assign to this buffer.\n     */\n    SampleBuffer &operator=(const SampleBuffer &o)\n    {\n        if (this != &o)  // Avoid self-assignment\n        {\n            if (_own_buffer && o._own_buffer)\n            {\n                if (_channel_count != o._channel_count)\n                {\n                    delete[] _buffer;\n                    _buffer = (o._channel_count > 0)? (new float[size * o._channel_count]) : nullptr;\n                    _channel_count = o._channel_count;\n                }\n            }\n            else\n            {\n                /* Assigning to or from a non owning buffer is only allowed if their\n                 * channel count matches. In that case the underlying sample data is\n                 * copied.\n                 * If their sample counts differs this might trigger a (re)allocation\n                 * of the internal data buffer, This would need to be resolved by\n                 * either forcing the SampleBuffer owning the data to change its\n                 * channel count or turn the non-owning SampleBuffer into a normal\n                 * SampleBuffer that owns its data buffer, and hence losing the\n                 * connection to the SampleBuffer that originally owned the data.\n                 * Both of which will have unexpected, and most likely unwanted, side\n                 * effects. */\n                assert(_channel_count == o._channel_count);\n            }\n            std::copy(o._buffer, o._buffer + (size * o._channel_count), _buffer);\n        }\n        return *this;\n    }\n\n    /**\n     * @brief Assign to this buffer using move semantics.\n     */\n    SampleBuffer &operator=(SampleBuffer &&o) noexcept\n    {\n        if (this != &o)  // Avoid self-assignment\n        {\n            if (_own_buffer)\n            {\n                delete[] _buffer;\n            }\n            _channel_count = o._channel_count;\n            _own_buffer = o._own_buffer;\n            _buffer = o._buffer;\n            o._buffer = nullptr;\n        }\n        return *this;\n    }\n\n    /**\n     * @brief Create a SampleBuffer from another SampleBuffer, without copying or\n     *        taking ownership of the data. Optionally only a subset of\n     *        the source buffers channels can be wrapped.\n     * @param source The SampleBuffer whose data is wrapped.\n     * @param start_channel The first channel to wrap. Defaults to 0.\n     * @param number_of_channels Must not exceed the channel count of the source buffer\n     *                           minus start_channel.\n     * @return The created, non-owning SampleBuffer.\n     */\n    static SampleBuffer create_non_owning_buffer(SampleBuffer& source,\n                                                 int start_channel,\n                                                 int number_of_channels)\n    {\n        assert(number_of_channels + start_channel <= source._channel_count);\n\n        SampleBuffer buffer;\n        buffer._own_buffer = false;\n        buffer._channel_count = number_of_channels;\n        buffer._buffer = source._buffer + size * start_channel;\n        return buffer;\n    }\n\n    /**\n     * @brief Defaulted version of the above function.\n     */\n    static SampleBuffer create_non_owning_buffer(SampleBuffer& source)\n    {\n        return create_non_owning_buffer(source, 0, source.channel_count());\n    }\n\n    /**\n     * @brief Create a SampleBuffer by wrapping a raw data pointer.\n     *\n     * @param data raw pointer to data stored in the same format of SampleBuffer storage\n     * @param start_channel Index of first channel to wrap.\n     * @param start_channel The first channel to wrap. Defaults to 0.\n     * @param number_of_channels Must not exceed the channel count of the source buffer\n     *                           minus start_channel.\n     * @return The created, non-owning SampleBuffer.\n     */\n    static SampleBuffer create_from_raw_pointer(float* data,\n                                                int start_channel,\n                                                int number_of_channels)\n    {\n        SampleBuffer buffer;\n        buffer._own_buffer = false;\n        buffer._channel_count = number_of_channels;\n        buffer._buffer = data + size * start_channel;\n        return buffer;\n    }\n\n    /**\n     * @brief Zero the entire buffer\n     */\n    void clear()\n    {\n        std::fill(_buffer, _buffer + (size * _channel_count), 0.0f);\n    }\n\n    /**\n    * @brief Returns a writeable pointer to a specific channel in the buffer. No bounds checking.\n    */\n    float* channel(int channel)\n    {\n        return _buffer + channel * size;\n    }\n\n    /**\n    * @brief Returns a read-only pointer to a specific channel in the buffer. No bounds checking.\n    */\n    const float* channel(int channel) const\n    {\n        return _buffer + channel * size;\n    }\n\n    /**\n     * @brief Gets the number of channels in the buffer.\n     */\n    int channel_count() const\n    {\n        return _channel_count;\n    }\n\n    /**\n     * @brief Copy interleaved audio data from interleaved_buf to this buffer.\n     */\n    void from_interleaved(const float* interleaved_buf)\n    {\n        switch (_channel_count)\n        {\n            case 2:  // Most common case, others are mostly included for future compatibility\n            {\n                float* l_in = _buffer;\n                float* r_in = _buffer + size;\n                for (int n = 0; n < size; ++n)\n                {\n                    *l_in++ = *interleaved_buf++;\n                    *r_in++ = *interleaved_buf++;\n                }\n                break;\n            }\n            case 1:\n            {\n                std::copy(interleaved_buf, interleaved_buf + size, _buffer);\n                break;\n            }\n            default:\n            {\n                for (int n = 0; n < size; ++n)\n                {\n                    for (int c = 0; c < _channel_count; ++c)\n                    {\n                        _buffer[n + c * _channel_count] = *interleaved_buf++;\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * @brief Copy buffer data in interleaved format to interleaved_buf\n     */\n    void to_interleaved(float* interleaved_buf) const\n    {\n        switch (_channel_count)\n        {\n            case 2:  // Most common case, others are mostly included for future compatibility\n            {\n                float* l_out = _buffer;\n                float* r_out = _buffer + size;\n                for (int n = 0; n < size; ++n)\n                {\n                    *interleaved_buf++ = *l_out++;\n                    *interleaved_buf++ = *r_out++;\n                }\n                break;\n            }\n            case 1:\n            {\n                std::copy(_buffer, _buffer + size, interleaved_buf);\n                break;\n            }\n            default:\n            {\n                for (int n = 0; n < size; ++n)\n                {\n                    for (int c = 0; c < _channel_count; ++c)\n                    {\n                        *interleaved_buf++ = _buffer[n + c * size];\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * @brief Apply a fixed gain to the entire buffer.\n     */\n    void apply_gain(float gain)\n    {\n        for (int i = 0; i < size * _channel_count; ++i)\n        {\n            _buffer[i] *= gain;\n        }\n    }\n\n    /**\n    * @brief Apply a fixed gain to a given channel.\n    */\n    void apply_gain(float gain, int channel)\n    {\n        float* data = _buffer + size * channel;\n        for (int i = 0; i < size; ++i)\n        {\n            data[i] *= gain;\n        }\n    }\n\n    /**\n     * @brief Replace the contents of the buffer with that of another buffer\n     * @param source SampleBuffer with either 1 channel or the same number of\n     *               channels as the destination buffer\n     */\n    void replace(const SampleBuffer &source)\n    {\n        assert(source.channel_count() == 1 || source.channel_count() == this->channel_count());\n\n        if (source.channel_count() == 1) // mono input, copy to all dest channels\n        {\n            for (int channel = 0; channel < _channel_count; ++channel)\n            {\n                std::copy(source._buffer, source._buffer + size, _buffer + channel * size);\n            }\n        }\n        else\n        {\n            std::copy(source._buffer, source._buffer + _channel_count * size, _buffer);\n        }\n    }\n\n    /**\n     * @brief Copy data channel by channel into this buffer from source buffer. No bounds checking.\n     */\n    void replace(int dest_channel, int source_channel, const SampleBuffer &source)\n    {\n        assert(source_channel < source.channel_count() && dest_channel < this->channel_count());\n        std::copy(source.channel(source_channel),\n                  source.channel(source_channel) + size,\n                  _buffer + (dest_channel * size));\n    }\n\n    /**\n     * @brief Sums the content of source into this buffer.\n     * @param source SampleBuffer with either 1 channel or the same number of\n     *               channels as the destination buffer\n     */\n    void add(const SampleBuffer &source)\n    {\n        assert(source.channel_count() == 1 || source.channel_count() == this->channel_count());\n\n        if (source.channel_count() == 1) // mono input, add to all dest channels\n        {\n            for (int channel = 0; channel < _channel_count; ++channel)\n            {\n                float* dest = _buffer + size * channel;\n                for (int i = 0; i < size; ++i)\n                {\n                    dest[i] += source._buffer[i];\n                }\n            }\n        } else if (source.channel_count() == _channel_count)\n        {\n            for (int i = 0; i < size * _channel_count; ++i)\n            {\n                _buffer[i] += source._buffer[i];\n            }\n        }\n    }\n\n\n    /**\n     * @brief Sums one channel of source buffer into one channel of the buffer.\n     */\n    void add(int dest_channel, int source_channel, const SampleBuffer& source)\n    {\n        float* source_data = source._buffer + size * source_channel;\n        float* dest_data = _buffer + size * dest_channel;\n        for (int i = 0; i < size; ++i)\n        {\n            dest_data[i] += source_data[i];\n        }\n    }\n\n    /**\n     * @brief Sums the content of SampleBuffer source into this buffer after applying a gain.\n     *\n     * source has to be either a 1 channel buffer or have the same number of channels\n     * as the destination buffer.\n    */\n    void add_with_gain(const SampleBuffer &source, float gain)\n    {\n        assert(source.channel_count() == 1 || source.channel_count() == this->channel_count());\n\n        if (source.channel_count() == 1)\n        {\n            for (int channel = 0; channel < _channel_count; ++channel)\n            {\n                float* dest = _buffer + size * channel;\n                for (int i = 0; i < size; ++i)\n                {\n                    dest[i] += source._buffer[i] * gain;\n                }\n            }\n        } else if (source.channel_count() == _channel_count)\n        {\n            for (int i = 0; i < size * _channel_count; ++i)\n            {\n                _buffer[i] += source._buffer[i] * gain;\n            }\n        }\n    }\n\n    /**\n     * @brief Sums one channel of source buffer into one channel of the buffer after applying gain.\n     */\n    void add_with_gain(int dest_channel, int source_channel, const SampleBuffer& source, float gain)\n    {\n        float* source_data = source._buffer + size * source_channel;\n        float* dest_data = _buffer + size * dest_channel;\n        for (int i = 0; i < size; ++i)\n        {\n            dest_data[i] += source_data[i] * gain;\n        }\n    }\n\n    /**\n     * @brief Sums the content of SampleBuffer source into this buffer after applying a\n     *        linear gain ramp.\n     *\n     * @param source The buffer to copy from. Has to be either a 1 channel buffer or have\n     *        the same number of channels as this buffer\n     * @param start The value to start the ramp from\n     * @param end The value to end the ramp\n    */\n    void add_with_ramp(const SampleBuffer &source, float start, float end)\n    {\n        assert(source.channel_count() == 1 || source.channel_count() == _channel_count);\n\n        float inc = (end - start) / (size - 1);\n        if (source.channel_count() == 1)\n        {\n            for (int channel = 0; channel < _channel_count; ++channel)\n            {\n                float* dest = _buffer + size * channel;\n                for (int i = 0; i < size; ++i)\n                {\n                    dest[i] += source._buffer[i] * (start + i * inc);\n                }\n            }\n        } else if (source.channel_count() == _channel_count)\n        {\n            for (int channel = 0; channel < _channel_count; ++channel)\n            {\n                float* source_data = _buffer + size * channel;\n                float* dest_data = _buffer + size * channel;\n                for (int i = 0; i < size; ++i)\n                {\n                    dest_data[i] += source_data[i] * (start + i * inc);\n                }\n            }\n        }\n    }\n\n    /**\n    * @brief Sums one channel of source buffer into one channel of the buffer after applying\n    *        a linear gain ramp.\n    */\n    void add_with_ramp(int dest_channel, int source_channel, const SampleBuffer& source, float start, float end)\n    {\n        float inc = (end - start) / (size - 1);\n        float* source_data = source._buffer + size * source_channel;\n        float* dest_data = _buffer + size * dest_channel;\n        for (int i = 0; i < size; ++i)\n        {\n            dest_data[i] += source_data[i] * (start + i * inc);\n        }\n    }\n\n    /**\n     * @brief Ramp the volume of all channels linearly from start to end\n     * @param start The value to start the ramp from\n     * @param end The value to end the ramp\n     */\n    void ramp(float start, float end)\n    {\n        float inc = (end - start) / (size - 1);\n        for (int channel = 0; channel < _channel_count; ++channel)\n        {\n            float* data = _buffer + size * channel;\n            for (int i = 0; i < size; ++i)\n            {\n                data[i] *= start + static_cast<float>(i) * inc;\n            }\n        }\n    }\n\n    /**\n     * @brief Convenience wrapper for ramping up from 0 to unity\n     */\n    void ramp_up()\n    {\n        this->ramp(0.0f, 1.0f);\n    }\n\n    /**\n     * @brief Convenience wrapper for ramping from unity down to 0\n     */\n    void ramp_down()\n    {\n        this->ramp(1.0f, 0.0f);\n    }\n\n    /**\n     * @brief Count the number of samples outside of [-1.0, 1.0] in one channel\n     * @param channel The channel to analyse, must not exceed the buffer's channel count\n     * @return The number of samples in the buffer whose absolute value is > 1.0\n     */\n    int count_clipped_samples(int channel) const\n    {\n        assert(channel < _channel_count);\n        int clip_count = 0;\n        const float* data = _buffer + size * channel;\n        for (int i = 0 ; i < size; ++i)\n        {\n            /* std::abs() is more efficient than testing for upper and lower bound separately\n               And GCC can compile this to vectorised, branchless code */\n            clip_count += std::abs(data[i]) >= 1.0f;\n        }\n        return clip_count;\n    }\n\n    /**\n     * @brief Calculate the peak value / loudest sample for one channel\n     * @param channel The channel to analyse, must not exceed the buffer's channel count\n     * @return The absolute value of the loudest sample\n     */\n    float calc_peak_value(int channel) const\n    {\n        assert(channel < _channel_count);\n        float max = 0.0f;\n        const float* data = _buffer + size * channel;\n        for (int i = 0 ; i < size; ++i)\n        {\n            max = std::max(max, std::abs(data[i]));\n        }\n        return max;\n    }\n\n    /**\n     * @brief Calculate the root-mean-square average for one channel\n     * @param channel The channel to analyse, must not exceed the buffer's channel count\n     * @return The RMS value of all the samples in the channel\n     */\n    float calc_rms_value(int channel) const\n    {\n        assert(channel < _channel_count);\n        float sum = 0.0f;\n        const float* data = _buffer + size * channel;\n        for (int i = 0 ; i < size; ++i)\n        {\n            float s = data[i];\n            sum += s * s;\n        }\n        return std::sqrt(sum / AUDIO_CHUNK_SIZE);\n    }\n\nprivate:\n    int _channel_count;\n    bool _own_buffer;\n    float* _buffer;\n\n    friend void swap<>(SampleBuffer<size>& lhs, SampleBuffer<size>& rhs);\n};\n\ntypedef SampleBuffer<AUDIO_CHUNK_SIZE> ChunkSampleBuffer;\n\n} // end namespace sushi\n\n\n#endif // SUSHI_SAMPLEBUFFER_H\n"
  },
  {
    "path": "include/sushi/standalone_factory.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Public Sushi factory for standalone use.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef STANDALONE_FACTORY_H\n#define STANDALONE_FACTORY_H\n\n#include \"factory_interface.h\"\n#include \"sushi.h\"\n\nnamespace sushi {\n\nnamespace internal {\n    class StandaloneFactoryImplementation;\n}\n\nclass StandaloneFactory : public FactoryInterface\n{\npublic:\n    StandaloneFactory();\n    ~StandaloneFactory() override;\n\n    [[nodiscard]] std::pair<std::unique_ptr<Sushi>, Status> new_instance(SushiOptions& options) override;\n\nprivate:\n    std::unique_ptr<internal::StandaloneFactoryImplementation> _implementation;\n};\n\n} // end namespace sushi\n\n#endif // STANDALONE_FACTORY_H\n"
  },
  {
    "path": "include/sushi/sushi.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Main entry point to Sushi\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SUSHI_INTERFACE_H\n#define SUSHI_SUSHI_INTERFACE_H\n\n#include <cassert>\n#include <memory>\n#include <chrono>\n#include <optional>\n#include <filesystem>\n#include <vector>\n\n#include \"compile_time_settings.h\"\n\nnamespace sushi {\n\nnamespace control {\nclass SushiControl;\n}\n\nnamespace internal::engine {\nclass AudioEngine;\n}\n\nnamespace internal::audio_frontend {\nstruct BaseAudioFrontendConfiguration;\nclass BaseAudioFrontend;\n}\n\nnamespace internal::midi_frontend {\nclass BaseMidiFrontend;\nclass ReactiveMidiFrontend;\n}\n\nenum class FrontendType\n{\n    OFFLINE,\n    DUMMY,\n    JACK,\n    PORTAUDIO,\n    APPLE_COREAUDIO,\n    XENOMAI_RASPA,\n    REACTIVE,\n    NONE\n};\n\nenum class ConfigurationSource : int\n{\n    NONE = 0,\n    FILE = 1,\n    JSON_STRING = 2\n};\n\n/**\n * The status of why starting Sushi failed.\n * The non-zero values are also returned by the process on exit\n */\nenum class Status : int\n{\n    OK = 0,\n\n    FAILED_INVALID_FILE_PATH = 1,\n    FAILED_INVALID_CONFIGURATION_FILE = 2,\n\n    FAILED_LOAD_HOST_CONFIG = 3,\n    FAILED_LOAD_TRACKS = 4,\n    FAILED_LOAD_MIDI_MAPPING = 5,\n    FAILED_LOAD_CV_GATE = 6,\n    FAILED_LOAD_PROCESSOR_STATES = 7,\n    FAILED_LOAD_EVENT_LIST = 8,\n    FAILED_LOAD_EVENTS = 9,\n    FAILED_LOAD_OSC = 10,\n\n    FAILED_XENOMAI_INITIALIZATION = 11,\n    FAILED_OSC_FRONTEND_INITIALIZATION = 12,\n    FAILED_AUDIO_FRONTEND_MISSING = 13,\n    FAILED_AUDIO_FRONTEND_INITIALIZATION = 14,\n    FAILED_MIDI_FRONTEND_INITIALIZATION = 15,\n\n    FAILED_TO_START_RPC_SERVER = 16,\n    FRONTEND_IS_INCOMPATIBLE_WITH_STANDALONE = 17,\n    \n    SUSHI_ALREADY_STARTED = 18,\n    SUSHI_THREW_EXCEPTION = 19,\n\n    UNINITIALIZED = 20\n};\n\nstd::string to_string(Status status);\n\n/**\n * Collects all options for instantiating Sushi in one place.\n */\nstruct SushiOptions\n{\n    /**\n     * Set this to choose what audio frontend type Sushi should use.\n     * The options are defined in the FrontendType enum class.\n     */\n    FrontendType frontend_type = FrontendType::NONE;\n\n    /**\n     * Specify a directory to be the base of plugin paths used in JSON configuration files,\n     * and over gRPC commands for plugin loading.\n     */\n    std::string base_plugin_path = std::filesystem::current_path().string();\n\n    /**\n     * Set this to choose how Sushi will be configured:\n     * By a json-config file path (ConfigurationSource::FILE),\n     * a string containing a json configuration (ConfigurationSource::JSON_STRING),\n     * or no configuration (ConfigurationSource::NONE)\n     * - in which case the minimum default config is set.\n     */\n    ConfigurationSource config_source = ConfigurationSource::FILE;\n\n    /**\n     * Only used if config_source is set to ConfigurationSource::FILE.\n     */\n    std::string config_filename = std::string(SUSHI_JSON_FILENAME_DEFAULT);\n\n    /**\n     * Only used if config_source is set to ConfigurationSource::JSON_STRING.\n     */\n    std::string json_string = std::string(SUSHI_JSON_STRING_DEFAULT);\n\n    /**\n     * Specify minimum logging level, from ('debug', 'info', 'warning', 'error')\n     */\n    std::string log_level = std::string(ELKLOG_LOG_LEVEL_DEFAULT);\n\n    /**\n     * Specify logging file destination.\n     */\n    std::string log_file = std::string(ELKLOG_LOG_FILE_DEFAULT);\n\n    /**\n     * These are only for the case of using a JACK audio frontend,\n     * and even then, you will rarely need to alter the defaults.\n     */\n    std::string jack_client_name = std::string(SUSHI_JACK_CLIENT_NAME_DEFAULT);\n    std::string jack_server_name = std::string(\"\");\n\n    /**\n     * Try to automatically connect Jack ports at startup.\n     */\n    bool connect_ports = false;\n\n    /**\n     * Index of the device to use for audio input with portaudio frontend [default=system default].\n     */\n    std::optional<int> portaudio_input_device_id = std::nullopt;\n\n    /**\n     * Index of the device to use for audio output with portaudio frontend [default=system default]\n     */\n    std::optional<int> portaudio_output_device_id = std::nullopt;\n\n    /**\n     * UID of the device to use for audio input with Apple CoreAudio frontend.\n     */\n    std::optional<std::string> apple_coreaudio_input_device_uid = std::nullopt;\n\n    /**\n     * UID of the device to use for audio output with Apple CoreAudio frontend.\n     */\n    std::optional<std::string> apple_coreaudio_output_device_uid = std::nullopt;\n\n    /**\n     * Input latency in seconds to suggest to portaudio.\n     * Will be rounded up to closest available latency depending on audio API.\n     */\n    float suggested_input_latency = SUSHI_PORTAUDIO_INPUT_LATENCY_DEFAULT;\n\n    /**\n     * Output latency in seconds to suggest to portaudio.\n     * Will be rounded up to closest available latency depending on audio API.\n     */\n    float suggested_output_latency = SUSHI_PORTAUDIO_OUTPUT_LATENCY_DEFAULT;\n\n    /**\n     * If true, Sushi will dump available audio devices to stdout in JSON format, and immediately exit.\n     * This requires a frontend to be specified.\n     */\n    bool enable_audio_devices_dump = false;\n\n    /**\n     * Dump plugin and parameter data to stdout in JSON format.\n     * This will reflect the configuration currently loaded.\n     */\n    bool enable_parameter_dump = false;\n\n    /**\n     * Set this to false, to disable Open Sound Control completely.\n     */\n    bool use_osc = true;\n\n    /**\n     * If the OSC control frontend is enabled,\n     * you will also need to configure the IP and Ports it uses.\n     *\n     *\n     * The osc_server_port is the Port to listen for OSC messages on.\n     * If it is unavailable, Sushi will fail to start, returning:\n     * Status::FAILED_OSC_FRONTEND_INITIALIZATION.\n     */\n    int osc_server_port = SUSHI_OSC_SERVER_PORT_DEFAULT;\n\n    /**\n     * Port and IP to send OSC messages to.\n     */\n    int osc_send_port = SUSHI_OSC_SEND_PORT_DEFAULT;\n    std::string osc_send_ip = SUSHI_OSC_SEND_IP_DEFAULT;\n\n    /**\n     * Set this to false, to disable gRPC completely.\n     */\n    bool use_grpc = true;\n\n    /**\n     * gRPC listening address in the format: address:port. By default accepts incoming connections from all ip:s.\n     */\n    std::string grpc_listening_address = SUSHI_GRPC_LISTENING_PORT_DEFAULT;\n\n    /**\n     * Set the path to the crash handler to use for sentry reports.\n     */\n    std::string sentry_crash_handler_path = SUSHI_SENTRY_CRASH_HANDLER_PATH_DEFAULT;\n\n    /**\n     * Set the DSN that sentry should upload crash logs to.\n     */\n    std::string sentry_dsn = SUSHI_SENTRY_DSN_DEFAULT;\n\n    /**\n     * These are used only if Sushi uses an Offline audio frontend.\n     * Then, sushi uses the first path as its audio input,\n     * and the second as output.\n     */\n    std::string input_filename;\n    std::string output_filename;\n\n    /**\n     * Break to debugger if a mode switch is detected (Xenomai only).\n     */\n    bool debug_mode_switches = false;\n\n    /**\n     * Process audio multi-threaded, with n cores [default n=1 (off)].\n     */\n    int  rt_cpu_cores = 1;\n\n    /**\n     * Enable performance timings on all audio processors.\n     */\n    bool enable_timings = false;\n\n    /**\n     * Log timings for every audio callback on these processors. Results will be written as a csv file on exit.\n     * Only use for debug purposes, may store significant amounts of data.\n     */\n    std::vector<std::string> detailed_timing_log;\n    /**\n     * Enable flushing the log periodically and specify the interval.\n     */\n    bool enable_flush_interval = false;\n    std::chrono::seconds log_flush_interval = std::chrono::seconds(0);\n\n    /**\n     * Extracts the address string and port number from the grpc_listening_address string.\n     * @return A pair with address and port on success - nullopt on failure.\n     */\n    std::optional<std::pair<std::string, int>> grpc_address_and_port();\n\n    /**\n     * If sushi is to be started with gRPC, initialising it requires a valid gRPC port number.\n     * Using this method, it is possible to incrementally increase the number passed,\n     * to retry connecting.\n     * @param options the SushiOptions structure containing the gRPC address field.\n     * @return true if incrementing the value succeeded.\n     */\n    bool increment_grpc_port_number();\n\n    // This field is used internally by Sushi.\n    std::optional<std::string> device_name = std::nullopt;\n};\n\n/**\n * base Sushi class API.\n * To create a Sushi instance, use one of the factories provided, depending on the use-case required:\n * - ReactiveFactory\n * - StandaloneFactory\n * - OfflineFactory\n */\nclass Sushi\n{\nprotected:\n    Sushi() = default;\n\npublic:\n    virtual ~Sushi() = default;\n\n    /**\n     * Given Sushi is initialized successfully, call this before the audio callback is first invoked.\n     * This is only meant to be called once during the instance lifetime.\n     * @return Status will reflect if starting was successful, or what error occurred.\n     */\n    [[nodiscard]] virtual Status start() = 0;\n\n    /**\n     * Call to stop the Sushi instance.\n     * This is only meant to be called once during the instance lifetime.\n     */\n    virtual void stop() = 0;\n\n    /**\n     * @return an instance of the Sushi controller - assuming Sushi has first been initialized.\n     */\n    virtual control::SushiControl* controller() = 0;\n\n    /**\n     * Setting the sample rate.\n     * @param sample_rate\n     */\n    virtual void set_sample_rate(float sample_rate) = 0;\n\n    /**\n     * Querying the currently set sample-rate.\n     * @return\n     */\n    virtual float sample_rate() const = 0;\n};\n\n} // end namespace sushi\n\n#endif // SUSHI_SUSHI_INTERFACE_H\n"
  },
  {
    "path": "include/sushi/sushi_time.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Sushi time types and constants\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_TIME_H\n#define SUSHI_TIME_H\n\n#include <chrono>\n\nnamespace sushi {\n\n/**\n * @brief Type used for timestamps with micro second granularity\n */\ntypedef std::chrono::microseconds Time;\n\n/**\n * @brief Convenience shorthand for setting timestamp to 0, i.e. process event without delay.\n */\nconstexpr Time IMMEDIATE_PROCESS = std::chrono::microseconds(0);\n\n/**\n * @brief Get the current time, only for calling from the non-rt part.\n * @return A Time object containing the current time\n */\ninline Time get_current_time()\n{\n    return std::chrono::duration_cast<Time>(std::chrono::steady_clock::now().time_since_epoch());\n}\n\n} // end namespace sushi\n\n#endif //SUSHI_TIME_H\n"
  },
  {
    "path": "include/sushi/terminal_utilities.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Tools for Sushi if it is enclosed in a standalone host.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_TERMINAL_UTILITIES_H\n#define SUSHI_TERMINAL_UTILITIES_H\n\n#include <vector>\n\n#include \"sushi.h\"\n\nnamespace sushi {\n\nenum class ParseStatus\n{\n    OK,\n    ERROR,\n    MISSING_ARGUMENTS,\n    EXIT\n};\n\nstd::vector<std::string> tokenize_arg(const char* arg, char delimiter)\n{\n    std::vector<std::string> args;\n    auto str_arg = std::string_view(arg);\n    auto pos = str_arg.find(delimiter);\n    while (pos != std::string_view::npos)\n    {\n        args.emplace_back(str_arg.substr(0, pos));\n        str_arg = str_arg.substr(pos + 1, std::string_view::npos);\n        pos = str_arg.find(delimiter);\n    }\n    args.emplace_back(str_arg);\n    return args;\n}\n\nstatic void print_version_and_build_info()\n{\n    std::cout << \"\\nVersion \" << CompileTimeSettings::sushi_version << std::endl;\n\n    std::cout << \"Build options enabled: \";\n\n    for (const auto& o : CompileTimeSettings::enabled_build_options)\n    {\n        if (o != CompileTimeSettings::enabled_build_options.front())\n        {\n            std::cout << \", \";\n        }\n\n        std::cout << o;\n    }\n\n    std::cout << std::endl;\n\n    std::cout << \"Audio buffer size in frames: \" << CompileTimeSettings::audio_chunk_size << std::endl;\n    std::cout << \"Git commit: \" << CompileTimeSettings::git_commit_hash << std::endl;\n    std::cout << \"Built on: \" << CompileTimeSettings::build_timestamp << std::endl;\n}\n\nstatic ParseStatus parse_options(int argc, char* argv[], sushi::SushiOptions& options)\n{\n    optionparser::Stats cl_stats(usage, argc, argv);\n    std::vector<optionparser::Option> cl_options(cl_stats.options_max);\n    std::vector<optionparser::Option> cl_buffer(cl_stats.buffer_max);\n    optionparser::Parser cl_parser(usage, argc, argv, &cl_options[0], &cl_buffer[0]);\n\n    if (cl_parser.error())\n    {\n        return ParseStatus::ERROR;\n    }\n\n    if (cl_parser.optionsCount() == 0 || cl_options[OPT_IDX_HELP])\n    {\n        optionparser::printUsage(fwrite, stdout, usage);\n        return ParseStatus::MISSING_ARGUMENTS;\n    }\n\n    for (int i = 0; i < cl_parser.optionsCount(); i++)\n    {\n        optionparser::Option& opt = cl_buffer[static_cast<size_t>(i)];\n\n        try\n        {\n            switch(opt.index())\n            {\n                case OPT_IDX_HELP:\n                case OPT_IDX_UNKNOWN:\n                    // should be handled before arriving here\n                    assert(false);\n                    break;\n\n                case OPT_IDX_VERSION:\n                    {\n                        print_version_and_build_info();\n                        return ParseStatus::EXIT;\n                    }\n                    break;\n\n                case OPT_IDX_LOG_LEVEL:\n                    options.log_level.assign(opt.arg);\n                    break;\n\n                case OPT_IDX_LOG_FILE:\n                    options.log_file.assign(opt.arg);\n                    break;\n\n                case OPT_IDX_LOG_FLUSH_INTERVAL:\n                    options.log_flush_interval = std::chrono::seconds(std::strtol(opt.arg, nullptr, 0));\n                    options.enable_flush_interval = true;\n                    break;\n\n                case OPT_IDX_DUMP_PARAMETERS:\n                    options.enable_parameter_dump = true;\n                    break;\n\n                case OPT_IDX_CONFIG_FILE:\n                    options.config_filename.assign(opt.arg);\n                    break;\n\n                case OPT_IDX_USE_OFFLINE:\n                    options.frontend_type = FrontendType::OFFLINE;\n                    break;\n\n                case OPT_IDX_INPUT_FILE:\n                    options.input_filename.assign(opt.arg);\n                    break;\n\n                case OPT_IDX_OUTPUT_FILE:\n                    options.output_filename.assign(opt.arg);\n                    break;\n\n                case OPT_IDX_USE_DUMMY:\n                    options.frontend_type = FrontendType::DUMMY;\n                    break;\n\n                case OPT_IDX_USE_PORTAUDIO:\n                    options.frontend_type = FrontendType::PORTAUDIO;\n                    break;\n\n                case OPT_IDX_USE_APPLE_COREAUDIO:\n                    options.frontend_type = FrontendType::APPLE_COREAUDIO;\n                    break;\n\n                case OPT_IDX_AUDIO_INPUT_DEVICE:\n                    options.portaudio_input_device_id = std::stoi(opt.arg);\n                    break;\n\n                case OPT_IDX_AUDIO_OUTPUT_DEVICE:\n                    options.portaudio_output_device_id = std::stoi(opt.arg);\n                    break;\n\n                case OPT_IDX_AUDIO_INPUT_DEVICE_UID:\n                    options.apple_coreaudio_input_device_uid = opt.arg;\n                    break;\n\n                case OPT_IDX_AUDIO_OUTPUT_DEVICE_UID:\n                    options.apple_coreaudio_output_device_uid = opt.arg;\n                    break;\n\n                case OPT_IDX_PA_SUGGESTED_INPUT_LATENCY:\n                    options.suggested_input_latency = static_cast<float>(std::atof(opt.arg));\n                    break;\n\n                case OPT_IDX_PA_SUGGESTED_OUTPUT_LATENCY:\n                    options.suggested_output_latency = static_cast<float>(std::atof(opt.arg));\n                    break;\n\n                case OPT_IDX_DUMP_DEVICES:\n                    options.enable_audio_devices_dump = true;\n                    break;\n\n                case OPT_IDX_USE_JACK:\n                    options.frontend_type = FrontendType::JACK;\n                    break;\n\n                case OPT_IDX_CONNECT_PORTS:\n                    options.connect_ports = true;\n                    break;\n\n                case OPT_IDX_JACK_CLIENT:\n                    options.jack_client_name.assign(opt.arg);\n                    break;\n\n                case OPT_IDX_JACK_SERVER:\n                    options.jack_server_name.assign(opt.arg);\n                    break;\n\n                case OPT_IDX_USE_XENOMAI_RASPA:\n                    options.frontend_type = FrontendType::XENOMAI_RASPA;\n                    break;\n\n                case OPT_IDX_XENOMAI_DEBUG_MODE_SW:\n                    options.debug_mode_switches = true;\n                    break;\n\n                case OPT_IDX_MULTICORE_PROCESSING:\n                    options.rt_cpu_cores = std::stoi(opt.arg);\n                    break;\n\n                case OPT_IDX_TIMINGS_STATISTICS:\n                    options.enable_timings = true;\n                    break;\n\n                case OPT_IDX_DETAILED_TIMINGS:\n                    options.detailed_timing_log = tokenize_arg(opt.arg, ';');\n                    break;\n\n                case OPT_IDX_OSC_RECEIVE_PORT:\n                    options.osc_server_port = std::stoi(opt.arg);\n                    break;\n\n                case OPT_IDX_OSC_SEND_PORT:\n                    options.osc_send_port = std::stoi(opt.arg);\n                    break;\n\n                case OPT_IDX_OSC_SEND_IP:\n                    options.osc_send_ip = opt.arg;\n                    break;\n\n                case OPT_IDX_GRPC_LISTEN_ADDRESS:\n                    options.grpc_listening_address = opt.arg;\n                    break;\n\n                case OPT_IDX_NO_OSC:\n                    options.use_osc = false;\n                    break;\n\n                case OPT_IDX_NO_GRPC:\n                    options.use_grpc = false;\n                    break;\n\n                case OPT_IDX_BASE_PLUGIN_PATH:\n                    options.base_plugin_path = std::string(opt.arg);\n                    break;\n\n                case OPT_IDX_SENTRY_CRASH_HANDLER:\n                    options.sentry_crash_handler_path = opt.arg;\n                    break;\n\n                case OPT_IDX_SENTRY_DSN:\n                    options.sentry_dsn = opt.arg;\n                    break;\n\n                default:\n                    SushiArg::print_error(\"Unhandled option '\", opt, \"' \\n\");\n                    break;\n            }\n        }\n        // Standard exceptions for stoi are invalid_argument and out_of_range.\n        // But I use a catch-all just in case.\n        catch (const std::exception& e)\n        {\n            std::cout << \"Malformed terminal argument: \" << e.what() << \"\\n\";\n            return ParseStatus::ERROR;\n        }\n    }\n\n    if (options.enable_parameter_dump)\n    {\n        options.frontend_type = FrontendType::DUMMY;\n    }\n\n    if (options.output_filename.empty() && !options.input_filename.empty())\n    {\n        options.output_filename = options.input_filename + \"_proc.wav\";\n    }\n\n    return ParseStatus::OK;\n}\n\n} // end namespace Sushi\n\n#endif // SUSHI_TERMINAL_UTILITIES_H\n"
  },
  {
    "path": "include/sushi/types.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief General types and typedefs not suitable to put elsewhere\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_TYPES_H\n#define SUSHI_TYPES_H\n\n#include <array>\n#include <cstdint>\n#include <cstddef>\n\nnamespace sushi {\n\n/**\n * @brief General struct for passing opaque binary data in events or parameters/properties\n */\nstruct BlobData\n{\n    int      size{0};\n    uint8_t* data{nullptr};\n};\n\nconstexpr size_t MIDI_DATA_BYTE_SIZE = 4;\n/**\n * @brief Convenience type for passing midi messages by value\n */\ntypedef std::array<uint8_t, MIDI_DATA_BYTE_SIZE> MidiDataByte;\nstatic_assert(sizeof(MidiDataByte) == MIDI_DATA_BYTE_SIZE);\n\n/**\n * @brief Struct to represent a defined time signature\n */\nstruct TimeSignature\n{\n    int numerator;\n    int denominator;\n};\n\ninline bool operator==(const TimeSignature& lhs, const TimeSignature& rhs)\n{\n    return (lhs.denominator == rhs.denominator && lhs.numerator == rhs.numerator);\n}\n\ninline bool operator!=(const TimeSignature& lhs, const TimeSignature& rhs)\n{\n    return ! (lhs == rhs);\n}\n\n/**\n * @brief baseclass for objects that can returned from a realtime thread for destruction\n */\nclass RtDeletable\n{\npublic:\n    virtual ~RtDeletable();\n};\n\n/**\n * @brief Wrapper for using  native and standard library types with RtDeletable\n */\ntemplate <typename T>\nstruct RtDeletableWrapper : public RtDeletable\n{\npublic:\n    explicit RtDeletableWrapper(T data) : _data(data) {}\n    ~RtDeletableWrapper() override = default;\n\n    T& data() {return _data;}\n    const T& data() const {return _data;}\n\nprivate:\n    T _data;\n};\n\n} // end namespace sushi\n\n#endif //SUSHI_TYPES_H\n"
  },
  {
    "path": "include/sushi/utils.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utility functions around rapidjson library\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_UTILS_H\n#define SUSHI_UTILS_H\n\n#include <iostream>\n#include <optional>\n\n#include \"rapidjson/document.h\"\n\nnamespace sushi {\n\nstruct SushiOptions;\n\nstd::ostream& operator<<(std::ostream& out, const rapidjson::Document& document);\n\n/**\n * Reads the file passed in through the path argument.\n * Returns true, and the contents on read success.\n * Or false and an empty string on failure.\n * @param path The absolut path to the file.\n * @return file content on success - nullopt on failure\n */\nstd::optional<std::string> read_file(const std::string& path);\n\n/**\n * This should be called only once in the lifetime of the embedding binary - or it will fail.\n * @param options\n */\nvoid init_logger([[maybe_unused]] const SushiOptions& options);\n\n}\n\n#endif // SUSHI_UTILS_H\n\n"
  },
  {
    "path": "include/version.h.in",
    "content": "#ifndef __version_h__\n#define __version_h__\n\n#define SUSHI__VERSION_MAJ  @SUSHI_VERSION_MAJOR@\n#define SUSHI__VERSION_MIN  @SUSHI_VERSION_MINOR@\n#define SUSHI__VERSION_REV  @SUSHI_VERSION_REVISION@\n\n#define SUSHI_GIT_COMMIT_HASH \"@GIT_COMMIT_HASH@\"\n#define SUSHI_BUILD_TIMESTAMP \"@BUILD_TIMESTAMP@\"\n#define SUSHI_EXTERNAL_API_VERSION \"@SUSHI_EXTERNAL_API_VERSION@\"\n\n#endif // #ifndef __version_h__\n"
  },
  {
    "path": "misc/.gitkeep",
    "content": ""
  },
  {
    "path": "misc/README.md",
    "content": "# Misc folder - example configurations\n\nThis folder contains some example JSON config files for Sushi, and some data (e.g. audio files) to run them.\n\nThe file \"elecguit.wav\" is taken from Freesound, licensed under the Creative Common Attribution license 4.0:\n\n[https://freesound.org/people/Jibey-/sounds/652043/](https://freesound.org/people/Jibey-/sounds/652043/)\n\nNot all the config files will work for every Sushi configuration, for example the multichannel or CV examples will require you to have enough I/O available. Their purpose is to cover most basic use cases that you could encounter.\n\nYou might need to change the absolute paths in some config files to make them work, e.g. the soundfile path in `play_brickworks_fx.json` or the plugin path in `play_vst2.json`.\n\nCopyright 2017-2023 Elk Audio AB, Stockholm\n\n"
  },
  {
    "path": "misc/config_files/arp_peakmeter_osc_broadcast.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"tempo_sync\" : \"ableton_link\",\n        \"playing_mode\" : \"playing\"\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [ ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.step_sequencer\",\n                    \"name\" : \"step\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"name\" : \"mda JX10\",\n                    \"type\" : \"vst3x\",\n                    \"uid\" : \"mda JX10\"\n                },\n                {\n                    \"uid\" : \"sushi.testing.peakmeter\",\n                    \"name\" : \"peakmeter\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"main\",\n                \"raw_midi\" : false\n            }\n        ]\n    },\n    \"osc\" : {\n        \"enabled_processor_outputs\" : [\n            {\n                \"processor\" : \"peakmeter\"\n            }\n        ]\n    },\n    \"initial_state\" : [\n        {\n            \"processor\" : \"main\",\n            \"parameters\" : {\n                \"gain\" : 0.7\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "misc/config_files/config_play_arp_link_midi_out.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"tempo_sync\" : \"ableton_link\",\n        \"playing_mode\" : \"playing\"\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [ ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.arpeggiator\",\n                    \"name\" : \"arp\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\": [\n            {\n                \"port\": 0,\n                \"channel\": \"all\",\n                \"track\": \"main\",\n                \"raw_midi\": false\n            }\n        ],\n        \"track_out_connections\": [\n            {\n                \"port\": 0,\n                \"channel\": 0,\n                \"track\": \"main\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "misc/config_files/cv_lfo.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"cv_inputs\" : 0,\n        \"cv_outputs\" :4\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [\n            ],\n            \"outputs\" : [\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.lfo\",\n                    \"name\" : \"lfo\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n    },\n    \"cv_control\" : {\n        \"cv_inputs\" : [\n        ],\n        \"cv_outputs\" : [\n            {\n                \"cv\" : 0,\n                \"processor\" : \"lfo\",\n                \"parameter\" : \"out\"\n            }\n        ],\n        \"gate_inputs\" : [ ],\n        \"gate_outputs\" : [ ]\n    }\n}\n"
  },
  {
    "path": "misc/config_files/cv_to_cutoff.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"cv_inputs\" : 2,\n        \"cv_outputs\" :4\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.arpeggiator\",\n                    \"name\" : \"arp\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"name\" : \"mda JX10\",\n                    \"type\" : \"vst3x\",\n                    \"uid\" : \"mda JX10\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"main\",\n                \"raw_midi\" : false\n            }\n        ]\n    },\n    \"cv_control\" : {\n        \"cv_inputs\" : [\n            {\n                \"cv\" : 0,\n                \"processor\" : \"mda JX10\",\n                \"parameter\" : \"VCF Freq\"\n            }\n        ],\n        \"cv_outputs\" : [],\n        \"gate_inputs\" : [],\n        \"gate_outputs\" : []\n    }\n}\n"
  },
  {
    "path": "misc/config_files/cv_to_synth.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"cv_inputs\" : 2,\n        \"cv_outputs\" : 0\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.cv_to_control\",\n                    \"name\" : \"cv-conv\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"name\" : \"mda JX10\",\n                    \"type\" : \"vst3x\",\n                    \"uid\" : \"mda JX10\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n\n    },\n    \"cv_control\" : {\n        \"cv_inputs\" : [\n            {\n                \"cv\" : 0,\n                \"processor\" : \"cv-conv\",\n                \"parameter\" : \"pitch_0\"\n            },\n            {\n                \"cv\": 1,\n                \"processor\" : \"mda JX10\",\n                \"parameter\" : \"VCF Freq\"\n            }\n        ],\n        \"cv_outputs\" : [\n\n        ],\n        \"gate_inputs\" : [  {\n            \"mode\" : \"note_event\",\n            \"processor\" : \"cv-conv\",\n            \"gate\" : 0,\n            \"note_no\" : 0,\n            \"channel\" : 0\n        } ],\n        \"gate_outputs\" : [\n\n        ]\n    }\n}\n"
  },
  {
    "path": "misc/config_files/empty.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.passthrough\",\n                    \"name\" : \"passthrough\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"cc_mappings\" : [\n        ]\n    }\n}\n"
  },
  {
    "path": "misc/config_files/freeverb_aux.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"playing_mode\" : \"playing\"\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.wav_streamer\",\n                    \"name\" : \"streamer\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.testing.send\",\n                    \"name\" : \"reverb_send\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        },\n        {\n            \"name\" : \"aux\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.return\",\n                    \"name\" : \"return\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.testing.freeverb\",\n                    \"name\" : \"freeverb\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"initial_state\" : [\n        {\n            \"processor\" : \"reverb_send\",\n            \"properties\" : {\n                \"destination_name\" : \"return\"\n            }\n        },\n        {\n            \"processor\" : \"streamer\",\n            \"properties\" : {\n                \"file\" : \"/home/mind/soundfiles/elecguit.wav\"\n            },\n            \"parameters\" : {\n                \"playing\" : 1,\n                \"loop\" : 1,\n                \"volume\" : 0.73\n            }\n        },\n        {\n            \"processor\" : \"freeverb\",\n            \"parameters\" : {\n                \"dry\" : 0,\n                \"wet\" : 1\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "misc/config_files/fx.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda Overdrive\",\n                    \"name\" : \"mda Overdrive\",\n                    \"type\" : \"vst3x\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda DubDelay\",\n                    \"name\" : \"mda DubDelay\",\n                    \"type\" : \"vst3x\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda Delay\",\n                    \"name\" : \"mda Delay\",\n                    \"type\" : \"vst3x\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda Ambience\",\n                    \"name\" : \"mda Ambience\",\n                    \"type\" : \"vst3x\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n    }\n}\n"
  },
  {
    "path": "misc/config_files/multi_track.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"guitar\",\n            \"channels\" : 2,\n            \"inputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda Combo\",\n                    \"name\" : \"mda Combo\",\n                    \"type\" : \"vst3x\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda Bandisto\",\n                    \"name\" : \"mda Bandisto\",\n                    \"type\" : \"vst3x\"\n                }\n            ]\n        },\n        {\n            \"name\" : \"synth\",\n            \"channels\" : 2,\n            \"inputs\" : [\n            ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda EPiano\",\n                    \"name\" : \"mda EPiano\",\n                    \"type\" : \"vst3x\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda RezFilter\",\n                    \"name\" : \"mda RezFilter\",\n                    \"type\" : \"vst3x\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda Ambience\",\n                    \"name\" : \"mda Ambience\",\n                    \"type\" : \"vst3x\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda Delay\",\n                    \"name\" : \"mda Delay\",\n                    \"type\" : \"vst3x\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"synth\",\n\t\t        \"raw_midi\" : true\n            }\n        ],\n        \"program_change_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"plugin\" : \"synth\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "misc/config_files/multichannel_plugin.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"multibus\" : true,\n            \"buses\" : 3,\n            \"inputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                },\n                {\n                    \"engine_bus\" : 1,\n                    \"track_bus\" : 1\n                }\n            ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                },\n                {\n                    \"engine_bus\" : 1,\n                    \"track_bus\" : 1\n                },\n                {\n                    \"engine_bus\" : 2,\n                    \"track_bus\" : 2\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.gain\",\n                    \"name\" : \"gain\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"main\",\n                \"raw_midi\" : false\n            }\n        ],\n        \"cc_mappings\" : [\n        ]\n    }\n}\n"
  },
  {
    "path": "misc/config_files/play_arp_mda_link.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"tempo_sync\" : \"ableton_link\",\n        \"playing_mode\" : \"playing\"\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [ ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.arpeggiator\",\n                    \"name\" : \"arp\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"name\" : \"mda JX10\",\n                    \"type\" : \"vst3x\",\n                    \"uid\" : \"mda JX10\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"main\",\n                \"raw_midi\" : false\n            }\n        ],\n        \"program_change_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"plugin\" : \"mda JX10\"\n            }\n        ]\n    },\n    \"initial_state\" : [\n        {\n            \"processor\" : \"main\",\n            \"parameters\" : {\n                \"gain\" : 0.7\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "misc/config_files/play_brickworks_fx.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"playing_mode\" : \"playing\"\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.wav_streamer\",\n                    \"name\" : \"streamer\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.noise_gate\",\n                    \"name\" : \"noise_gate\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.highpass\",\n                    \"name\" : \"highpass\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.multi_filter\",\n                    \"name\" : \"multi_filter\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.saturation\",\n                    \"name\" : \"saturation\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.dist\",\n                    \"name\" : \"dist\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.drive\",\n                    \"name\" : \"drive\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.fuzz\",\n                    \"name\" : \"fuzz\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.bitcrusher\",\n                    \"name\" : \"bitcrusher\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.flanger\",\n                    \"name\" : \"flanger\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.phaser\",\n                    \"name\" : \"phaser\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.tremolo\",\n                    \"name\" : \"tremolo\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.vibrato\",\n                    \"name\" : \"vibrato\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.wah\",\n                    \"name\" : \"wah\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.chorus\",\n                    \"name\" : \"chorus\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.comb_delay\",\n                    \"name\" : \"comb_delay\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.compressor\",\n                    \"name\" : \"compressor\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.notch\",\n                    \"name\" : \"notch\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.eq3band\",\n                    \"name\" : \"eq3band\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.clip\",\n                    \"name\" : \"clip\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"initial_state\" : [\n        {\n            \"processor\" : \"streamer\",\n            \"properties\" : {\n                \"file\" : \"/home/mind/soundfiles/elecguit.wav\"\n            }\n        },\n        {\n            \"processor\" : \"streamer\",\n            \"parameters\" : {\n                \"playing\" : 1,\n                \"loop\" : 1\n            }\n        },\n        {\n            \"processor\" : \"noise_gate\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"highpass\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"multi_filter\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"saturation\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"dist\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"drive\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"fuzz\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"bitcrusher\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"flanger\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"phaser\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"tremolo\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"vibrato\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"wah\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"chorus\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"comb_delay\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"compressor\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"notch\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"eq3band\",\n            \"bypassed\" : true\n        },\n        {\n            \"processor\" : \"clip\",\n            \"bypassed\" : true\n        }\n    ]\n}\n"
  },
  {
    "path": "misc/config_files/play_brickworks_synth.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"tempo\" : 140.0,\n        \"playing_mode\" : \"playing\"\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.step_sequencer\",\n                    \"name\" : \"sequencer\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.brickworks.simple_synth\",\n                    \"name\" : \"synth\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"main\",\n                \"raw_midi\" : false\n            }\n        ]\n    },\n    \"initial_state\" : [\n        {\n            \"processor\" : \"sequencer\",\n            \"parameters\" : {\n                \"pitch_1\" : 0.41666666666666674,\n                \"pitch_2\" : 0.5,\n                \"pitch_3\" : 0.5625,\n                \"pitch_4\" : 0.6458333333333334,\n                \"pitch_5\" : 0.6666666666666667,\n                \"pitch_6\" : 0.6458333333333334,\n                \"pitch_7\" : 0.5625,\n                \"pitch_0\" : 0.5,\n                \"step_0\" : 1.0,\n                \"step_1\" : 1.0,\n                \"step_2\" : 1.0,\n                \"step_3\" : 1.0,\n                \"step_4\" : 1.0,\n                \"step_5\" : 1.0,\n                \"step_6\" : 1.0,\n                \"step_7\" : 1.0\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "misc/config_files/play_cv_arp.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"cv_inputs\" : 0,\n        \"cv_outputs\" : 4\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.arpeggiator\",\n                    \"name\" : \"arp\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.testing.control_to_cv\",\n                    \"name\" : \"cv-conv\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n\n    },\n    \"cv_control\" : {\n        \"cv_inputs\" : [ ],\n        \"cv_outputs\" : [\n            {\n                \"cv\" : 0,\n                \"processor\" : \"cv-conv\",\n                \"parameter\" : \"pitch_0\"\n            }\n        ],\n        \"gate_inputs\" : [ ],\n        \"gate_outputs\" : [\n            {\n                \"mode\" : \"note_event\",\n                \"processor\" : \"cv-conv\",\n                \"gate\" : 0,\n                \"note_no\" : 0,\n                \"channel\" : 0\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "misc/config_files/play_lv2.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"tempo_sync\" : \"internal\",\n        \"playing_mode\" : \"playing\"\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uri\" : \"http://lv2plug.in/plugins/eg-metro\",\n                    \"name\" : \"metro\",\n                    \"type\" : \"lv2\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"main\",\n                \"raw_midi\" : false\n            }\n        ]\n    }\n}"
  },
  {
    "path": "misc/config_files/play_lv2_jx10.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uri\" : \"http://drobilla.net/plugins/mda/JX10\",\n                    \"name\" : \"jx10\",\n                    \"type\" : \"lv2\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"main\",\n                \"raw_midi\" : false\n            }\n        ],\n        \"program_change_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"plugin\" : \"jx10\"\n            }\n        ]\n    }\n}"
  },
  {
    "path": "misc/config_files/play_master_gain.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"playing_mode\" : \"playing\"\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.arpeggiator\",\n                    \"name\" : \"arp\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.testing.sampleplayer\",\n                    \"name\" : \"sampler\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"post_track\" : {\n        \"name\" : \"master_gain\",\n        \"plugins\" : [\n            {\n                \"uid\" : \"sushi.testing.gain\",\n                \"name\" : \"gain\",\n                \"type\" : \"internal\"\n            }\n        ]\n    },\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"main\",\n                \"raw_midi\" : false\n            }\n        ]\n    },\n    \"initial_state\" : [\n        {\n            \"processor\" : \"sampler\",\n            \"properties\" : {\n                \"sample_file\" : \"../test/data/Kawai-K11-GrPiano-C4_mono.wav\"\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "misc/config_files/play_vst2.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 44100,\n        \"playing_mode\" : \"playing\",\n        \"tempo_sync\" : \"internal\"\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.step_sequencer\",\n                    \"name\" : \"arp\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"path\" : \"mda-vst2/mdaJX10.so\",\n                    \"name\" : \"synth\",\n                    \"type\" : \"vst2x\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"main\",\n                \"raw_midi\" : true\n            }\n        ],\n        \"program_change_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"plugin\" : \"synth\"\n            }\n        ],\n        \"cc_mappings\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : 0,\n                \"cc_number\" : 22,\n                \"plugin_name\" : \"synth\",\n                \"parameter_name\" : \"VCF Freq\",\n                \"min_range\" : 0,\n                \"max_range\" : 1\n            },\n            {\n                \"port\" : 0,\n                \"channel\" : 0,\n                \"cc_number\" : 23,\n                \"plugin_name\" : \"synth\",\n                \"parameter_name\" : \"VCF Reso\",\n                \"min_range\" : 0,\n                \"max_range\" : 1\n            }\n        ]\n    },\n    \"osc\" : {\n        \"enable_all_processor_outputs\" : true\n    }\n}\n"
  },
  {
    "path": "misc/config_files/play_vst3.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"name\" : \"mda JX10\",\n                    \"type\" : \"vst3x\",\n                    \"uid\" : \"mda JX10\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"main\",\n                \"raw_midi\" : false\n            }\n        ],\n        \"program_change_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"plugin\" : \"mda JX10\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "misc/config_files/play_wav_streamer.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"tempo_sync\" : \"internal\",\n        \"playing_mode\" : \"playing\"\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.wav_streamer\",\n                    \"name\" : \"streamer\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"initial_state\" : [\n        {\n            \"processor\" : \"streamer\",\n            \"properties\" : {\n                \"file\" : \"/home/mind/music/wave_file.wav\"\n            }\n        }\n    ]\n}"
  },
  {
    "path": "misc/config_files/prepost_tracks.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000\n    },\n    \"osc\" : {\n        \"enable_all_processor_outputs\" : true\n    },\n    \"pre_track\" : {\n        \"name\" : \"pre\",\n        \"plugins\" : [\n            {\n                \"uid\" : \"sushi.testing.gain\",\n                \"name\" : \"pregain\",\n                \"type\" : \"internal\"\n            }\n        ]\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"outputs\" : [\n                {\n                    \"track_bus\" : 0,\n                    \"engine_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.equalizer\",\n                    \"name\" : \"eq\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"post_track\" : {\n        \"name\" : \"post\",\n        \"plugins\" : [\n            {\n                \"uid\" : \"sushi.testing.peakmeter\",\n                \"name\" : \"peakmeter\",\n                \"type\" : \"internal\"\n            }\n        ]\n    }\n}\n\n"
  },
  {
    "path": "misc/config_files/rt_midi.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"audio_clip_detection\" : {\n            \"inputs\" : true,\n            \"outputs\" : true\n        },\n        \"rt_midi_input_mappings\" : [\n            {\n                \"rt_midi_device\" : 0,\n                \"sushi_midi_port\" : 0,\n                \"virtual_port\" : false\n            }\n        ],\n        \"rt_midi_output_mappings\" : [\n            {\n                \"rt_midi_device\" : 0,\n                \"sushi_midi_port\" : 0,\n                \"virtual_port\": false\n            }\n        ]\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"name\" : \"mda JX10\",\n                    \"type\" : \"vst3x\",\n                    \"uid\" : \"mda JX10\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"main\",\n                \"raw_midi\" : false\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "misc/config_files/send_return_seq.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"playing_mode\" : \"playing\"\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"analog\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.arpeggiator\",\n                    \"name\" : \"arp\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"name\" : \"mda JX10\",\n                    \"type\" : \"vst3x\",\n                    \"uid\" : \"mda JX10\"\n                },\n                {\n                    \"uid\" : \"sushi.testing.send\",\n                    \"name\" : \"send_analog\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        },\n        {\n            \"name\" : \"fm\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.step_sequencer\",\n                    \"name\" : \"fm_arp\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"name\" : \"mda DX10\",\n                    \"type\" : \"vst3x\",\n                    \"uid\" : \"mda DX10\"\n                },\n                {\n                    \"uid\" : \"sushi.testing.send\",\n                    \"name\" : \"send_fm\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        },\n        {\n            \"name\" : \"return_track\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.return\",\n                    \"name\" : \"return\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda Delay\",\n                    \"name\" : \"mda Delay\",\n                    \"type\" : \"vst3x\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"fm\",\n                \"raw_midi\" : false\n            }\n        ],\n        \"program_change_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"plugin\" : \"mda DX10\"\n            }\n        ],\n        \"cc_mappings\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : 0,\n                \"cc_number\" : 22,\n                \"plugin_name\" : \"mda JX10\",\n                \"parameter_name\" : \"VCF Freq\",\n                \"min_range\" : 0,\n                \"max_range\" : 1\n            },\n            {\n                \"port\" : 0,\n                \"channel\" : 0,\n                \"cc_number\" : 23,\n                \"plugin_name\" : \"mda JX10\",\n                \"parameter_name\" : \"VCF Reso\",\n                \"min_range\" : 0,\n                \"max_range\" : 1\n            }\n        ]\n    },\n    \"osc\" : {\n        \"enable_all_processor_outputs\" : true\n    },\n    \"initial_state\" : [\n        {\n            \"processor\" : \"send_analog\",\n            \"properties\" : {\n                \"destination_name\" : \"return\"\n            }\n        },\n        {\n            \"processor\" : \"send_fm\",\n            \"properties\" : {\n                \"destination_name\" : \"return\"\n            }\n        },\n        {\n            \"processor\" : \"mda Delay\",\n            \"parameters\" : {\n                \"L Delay\" : 0.7,\n                \"Fb Mix\" : 0.33\n            }\n        },\n        {\n            \"processor\" : \"mda DX10\",\n            \"program\" : 8,\n            \"parameters\" : {\n                \"Mod Init\" : 1.0,\n                \"Mod Dec\" : 0.0\n            }\n        },\n        {\n            \"processor\" : \"mda JX10\",\n            \"program\" : 8,\n            \"parameters\" : {\n                \"VCF Freq\" : 1.0,\n                \"VCF Reso\" : 0.0\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "misc/config_files/vst_8_ch.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main (Piano)\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda Piano\",\n                    \"name\" : \"mda Piano\",\n                    \"type\" : \"vst3x\"\n                }\n            ]\n        },\n        {\n            \"name\" : \"second (Synth)\",\n            \"channels\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 1,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"name\" : \"mda JX10\",\n                    \"type\" : \"vst3x\",\n                    \"uid\" : \"mda JX10\"\n                }\n            ]\n        },\n        {\n            \"name\" : \"third (Guitar)\",\n            \"channels\" : 2,\n            \"inputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 2,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda Combo\",\n                    \"name\" : \"mda Combo\",\n                    \"type\" : \"vst3x\"\n                },\n                {\n                    \"path\" : \"mda-vst3.vst3\",\n                    \"uid\" : \"mda Delay\",\n                    \"name\" : \"mda Delay\",\n                    \"type\" : \"vst3x\"\n                }\n            ]\n        },\n        {\n            \"name\" : \"fourth (Passthrough)\",\n            \"channels\" : 2,\n            \"inputs\" : [\n                {\n                    \"engine_bus\" : 1,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 3,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.gain\",\n                    \"name\" : \"gain\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"track_connections\" : [\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"main (Piano)\",\n                \"raw_midi\" : false\n            },\n            {\n                \"port\" : 0,\n                \"channel\" : \"all\",\n                \"track\" : \"second (Synth)\",\n                \"raw_midi\" : false\n            }\n        ],\n        \"cc_mappings\" : [\n        ]\n    }\n}\n"
  },
  {
    "path": "rpc_interface/CMakeLists.txt",
    "content": "####################\n#  CMake packages  #\n####################\n\nfind_package(gRPC REQUIRED)\n\n\n################################\n#  gRPC / Protobuf generation  #\n################################\n\nset(PROTOS protos/sushi_rpc.proto)\n\n# In yocto environments, yocto grpc does not include gRPCPluginTargets.cmake\n# when find_package(gRPC REQUIRED) is called.\nif(CMAKE_CROSSCOMPILING)\n    find_program(Protobuf_PROTOC_EXECUTABLE REQUIRED NAMES protoc)\n    if(NOT TARGET protobuf::protoc)\n        add_executable(protobuf::protoc IMPORTED)\n    endif()\n    set_target_properties(protobuf::protoc PROPERTIES\n        IMPORTED_LOCATION \"${Protobuf_PROTOC_EXECUTABLE}\")\n    find_program(_gRPC_CPP_PLUGIN grpc_cpp_plugin)\nelse()\n    set(_gRPC_CPP_PLUGIN \\$<TARGET_FILE:gRPC::grpc_cpp_plugin>)\nendif()\n\n\n# Intermediate library target with the generated protobuf objects\nadd_library(protos-lib OBJECT)\ntarget_sources(protos-lib PRIVATE ${PROTOS})\ntarget_link_libraries(protos-lib PUBLIC gRPC::grpc++)\ntarget_include_directories(protos-lib PUBLIC ${CMAKE_CURRENT_BINARY_DIR})\nprotobuf_generate(TARGET protos-lib LANGUAGE cpp)\nprotobuf_generate(\n    TARGET protos-lib\n    LANGUAGE grpc\n    GENERATE_EXTENSIONS .grpc.pb.h .grpc.pb.cc\n    PLUGIN\n    \"protoc-gen-grpc=${_gRPC_CPP_PLUGIN}\"\n)\n\n######################\n#  Library target    #\n######################\n\nset(SUSHI_GRPC_SOURCES src/grpc_server.cpp\n                       src/control_service.cpp\n                       src/async_service_call_data.cpp )\n\nset(SUSHI_RPC_INCLUDE_DIRS ${CMAKE_CURRENT_BINARY_DIR}/protos\n                           ${CMAKE_CURRENT_SOURCE_DIR}\n                           ${PROJECT_SOURCE_DIR}/include\n                           ${PROJECT_SOURCE_DIR}/src )\n\nadd_library(sushi_rpc STATIC ${SUSHI_GRPC_SOURCES})\n\ntarget_include_directories(sushi_rpc PUBLIC include)\ntarget_include_directories(sushi_rpc PRIVATE ${SUSHI_RPC_INCLUDE_DIRS})\ntarget_link_libraries(sushi_rpc PUBLIC gRPC::grpc++ protos-lib elk::warningSuppressor)\ntarget_compile_features(sushi_rpc PRIVATE cxx_std_17)\n\nif(MSVC)    \n    set(SUSHI_RPC_COMPILE_OPTIONS /W4)\nelse()\n    set(SUSHI_RPC_COMPILE_OPTIONS -Wall -Wextra -fPIC)\nendif()\n\ntarget_compile_options(sushi_rpc PRIVATE ${SUSHI_RPC_COMPILE_OPTIONS})\n\n##################\n#  Install step  #\n##################\n\ninstall(FILES ${PROTOS} DESTINATION ${CMAKE_INSTALL_PREFIX}/share/sushi)\n"
  },
  {
    "path": "rpc_interface/include/sushi_rpc/grpc_server.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief gRPC Server\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_GRPCSERVER_H\n#define SUSHI_GRPCSERVER_H\n\n#include <memory>\n#include <thread>\n#include <atomic>\n\n/* Forward declare grpc and service classes so their definitions can be\n * kept completely separate from the rest of the Sushi codebase. The macro\n * conditions ensure compatibility with ubuntu 20's 1.16 grpc version and the\n * 1.24 grpc version on Elk OS. */\n\n#if GOOGLE_PROTOBUF_VERSION > 3007001\nnamespace grpc_impl {\n#else\nnamespace grpc {\n#endif\n    class Server;\n    class ServerBuilder;\n    class ServerCompletionQueue;\n}\n\nnamespace sushi_rpc {\n\nclass SystemControlService;\nclass TransportControlService;\nclass TimingControlService;\nclass KeyboardControlService;\nclass AudioGraphControlService;\nclass ParameterControlService;\nclass ProgramControlService;\nclass MidiControlService;\nclass AudioRoutingControlService;\nclass OscControlService;\nclass SessionControlService;\nclass NotificationControlService;\n\nconstexpr std::chrono::duration SERVER_SHUTDOWN_DEADLINE = std::chrono::milliseconds(50);\n\nclass GrpcServer\n{\npublic:\n    GrpcServer(const std::string& listenAddress, sushi::control::SushiControl* controller);\n\n    ~GrpcServer();\n\n    /**\n     * Attempts to instantiate and start the gRPC server.\n     * On failure, this GrpcServer class is in an undefined state and may be un-salvageable.\n     * Delete it retry with a different listenAddress.\n     * @return bool, reflecting the status of the resulting server.\n     */\n    [[nodiscard]] bool start();\n\n    void stop();\n\n    void waitForCompletion();\n\n    void AsyncRpcLoop();\n\nprivate:\n    std::string                                     _listen_address;\n\n    std::unique_ptr<SystemControlService>           _system_control_service;\n    std::unique_ptr<TransportControlService>        _transport_control_service;\n    std::unique_ptr<TimingControlService>           _timing_control_service;\n    std::unique_ptr<KeyboardControlService>         _keyboard_control_service;\n    std::unique_ptr<AudioGraphControlService>       _audio_graph_control_service;\n    std::unique_ptr<ParameterControlService>        _parameter_control_service;\n    std::unique_ptr<ProgramControlService>          _program_control_service;\n    std::unique_ptr<MidiControlService>             _midi_control_service;\n    std::unique_ptr<AudioRoutingControlService>     _audio_routing_control_service;\n    std::unique_ptr<OscControlService>              _osc_control_service;\n    std::unique_ptr<SessionControlService>          _session_control_service;\n    std::unique_ptr<NotificationControlService>     _notification_control_service;\n\n    std::unique_ptr<grpc::ServerBuilder>            _server_builder;\n    std::unique_ptr<grpc::Server>                   _server;\n    std::unique_ptr<grpc::ServerCompletionQueue>    _async_rpc_queue;\n    std::thread                                     _worker;\n    std::atomic<bool>                               _running;\n};\n\n}// sushi_rpc\n\n#endif //SUSHI_GRPCSERVER_H\n"
  },
  {
    "path": "rpc_interface/src/async_service_call_data.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Sushi Async gRPC Call Data implementation. Objects to handle async calls to sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"async_service_call_data.h\"\n\n#include \"control_service.h\"\n\nnamespace sushi_rpc {\n\ninline BlocklistKey create_key(int parameter_id, int processor_id)\n{\n    return (static_cast<BlocklistKey>(parameter_id) << 32) | processor_id;\n}\n\nvoid CallData::_alert()\n{\n    _in_completion_queue = true;\n    _alarm.Set(_async_rpc_queue, gpr_now(gpr_clock_type::GPR_CLOCK_REALTIME), this);\n}\n\nvoid CallData::stop()\n{\n    _status = CallStatus::FINISH;\n}\n\ntemplate<class ValueType, class BlocklistType>\nvoid SubscribeToUpdatesCallData<ValueType, BlocklistType>::proceed()\n{\n    if (_status == CallStatus::CREATE)\n    {\n        _status = CallStatus::PROCESS;\n        _subscribe();\n        _in_completion_queue = true;\n    }\n    else if (_status == CallStatus::PROCESS)\n    {\n        if (_first_iteration)\n        {\n            _respawn();\n            _active = true;\n            _populate_blocklist();\n            _first_iteration = false;\n        }\n\n        if (_notifications.empty() == false)\n        {\n            auto reply = _notifications.pop();\n            if (_check_if_blocklisted(*reply.get()) == false)\n            {\n                _in_completion_queue = true;\n                _responder.Write(*reply.get(), this);\n                _status = CallStatus::PUSH_TO_BACK;\n                return;\n            }\n        }\n        _in_completion_queue = false;\n    }\n    else if (_status == CallStatus::PUSH_TO_BACK)\n    {\n        // Since a call to write adds the object at the front of the completion\n        // queue. We then place it at the back with an alarm to serve the clients\n        // in a round robin fashion.\n        _status = CallStatus::PROCESS;\n        _alert();\n    }\n    else\n    {\n        assert(_status == CallStatus::FINISH);\n        _unsubscribe();\n        delete this;\n    }\n}\n\ntemplate<class ValueType, class BlocklistType>\nvoid SubscribeToUpdatesCallData<ValueType, BlocklistType>::push(std::shared_ptr<ValueType> notification)\n{\n    if (_active)\n    {\n        _notifications.push(notification);\n    }\n    if (_in_completion_queue == false)\n    {\n        _alert();\n    }\n}\n\n// Pre-Declaring what template instantiation is needed for SubscribeToUpdatesCallData,\n// or it will not link.\ntemplate class SubscribeToUpdatesCallData<TransportUpdate, GenericVoidValue>;\ntemplate class SubscribeToUpdatesCallData<CpuTimings, GenericVoidValue>;\ntemplate class SubscribeToUpdatesCallData<TrackUpdate, GenericVoidValue>;\ntemplate class SubscribeToUpdatesCallData<ProcessorUpdate, GenericVoidValue>;\ntemplate class SubscribeToUpdatesCallData<ParameterUpdate, ParameterNotificationBlocklist>;\ntemplate class SubscribeToUpdatesCallData<PropertyValue, PropertyNotificationBlocklist>;\ntemplate class SubscribeToUpdatesCallData<AsyncCommandResponse, GenericVoidValue>;\n\nvoid SubscribeToTransportChangesCallData::_respawn()\n{\n    new SubscribeToTransportChangesCallData(_service, _async_rpc_queue);\n}\n\nvoid SubscribeToTransportChangesCallData::_subscribe()\n{\n    _service->RequestSubscribeToTransportChanges(&_ctx,\n                                                 &_notification_blocklist,\n                                                 &_responder,\n                                                 _async_rpc_queue,\n                                                 _async_rpc_queue,\n                                                 this);\n    _service->subscribe(this);\n}\n\nvoid SubscribeToTransportChangesCallData::_unsubscribe()\n{\n    _service->unsubscribe(this);\n}\n\nbool SubscribeToTransportChangesCallData::_check_if_blocklisted(const TransportUpdate&)\n{\n    return false;\n}\n\nvoid SubscribeToCpuTimingUpdatesCallData::_respawn()\n{\n    new SubscribeToCpuTimingUpdatesCallData(_service, _async_rpc_queue);\n}\n\nvoid SubscribeToCpuTimingUpdatesCallData::_subscribe()\n{\n    _service->RequestSubscribeToEngineCpuTimingUpdates(&_ctx,\n                                                       &_notification_blocklist,\n                                                       &_responder,\n                                                       _async_rpc_queue,\n                                                       _async_rpc_queue,\n                                                       this);\n    _service->subscribe(this);\n}\n\nvoid SubscribeToCpuTimingUpdatesCallData::_unsubscribe()\n{\n    _service->unsubscribe(this);\n}\n\nbool SubscribeToCpuTimingUpdatesCallData::_check_if_blocklisted(const CpuTimings&)\n{\n    return false;\n}\n\nvoid SubscribeToTrackChangesCallData::_respawn()\n{\n    new SubscribeToTrackChangesCallData(_service, _async_rpc_queue);\n}\n\nvoid SubscribeToTrackChangesCallData::_subscribe()\n{\n    _service->RequestSubscribeToTrackChanges(&_ctx,\n                                             &_notification_blocklist,\n                                             &_responder,\n                                             _async_rpc_queue,\n                                             _async_rpc_queue,\n                                             this);\n    _service->subscribe(this);\n}\n\nvoid SubscribeToTrackChangesCallData::_unsubscribe()\n{\n    _service->unsubscribe(this);\n}\n\nbool SubscribeToTrackChangesCallData::_check_if_blocklisted(const TrackUpdate&)\n{\n    return false;\n}\n\nvoid SubscribeToProcessorChangesCallData::_respawn()\n{\n    new SubscribeToProcessorChangesCallData(_service, _async_rpc_queue);\n}\n\nvoid SubscribeToProcessorChangesCallData::_subscribe()\n{\n    _service->RequestSubscribeToProcessorChanges(&_ctx,\n                                                 &_notification_blocklist,\n                                                 &_responder,\n                                                 _async_rpc_queue,\n                                                 _async_rpc_queue,\n                                                 this);\n    _service->subscribe(this);\n}\n\nvoid SubscribeToProcessorChangesCallData::_unsubscribe()\n{\n    _service->unsubscribe(this);\n}\n\nbool SubscribeToProcessorChangesCallData::_check_if_blocklisted(const ProcessorUpdate&)\n{\n    return false;\n}\n\nvoid SubscribeToParameterUpdatesCallData::_respawn()\n{\n    new SubscribeToParameterUpdatesCallData(_service, _async_rpc_queue);\n}\n\nvoid SubscribeToParameterUpdatesCallData::_subscribe()\n{\n    _service->RequestSubscribeToParameterUpdates(&_ctx,\n                                                 &_notification_blocklist,\n                                                 &_responder,\n                                                 _async_rpc_queue,\n                                                 _async_rpc_queue,\n                                                 this);\n    _service->subscribe(this);\n}\n\nvoid SubscribeToParameterUpdatesCallData::_unsubscribe()\n{\n    _service->unsubscribe(this);\n}\n\nbool SubscribeToParameterUpdatesCallData::_check_if_blocklisted(const ParameterUpdate& reply)\n{\n    auto key = create_key(reply.parameter().parameter_id(), reply.parameter().processor_id());\n    return !(_blocklist.find(key) == _blocklist.end());\n}\n\nvoid SubscribeToParameterUpdatesCallData::_populate_blocklist()\n{\n    for (auto& identifier : _notification_blocklist.parameters())\n    {\n        _blocklist[create_key(identifier.parameter_id(), identifier.processor_id())] = false;\n    }\n}\n\nvoid SubscribeToPropertyUpdatesCallData::_respawn()\n{\n    new SubscribeToPropertyUpdatesCallData(_service, _async_rpc_queue);\n}\n\nvoid SubscribeToPropertyUpdatesCallData::_subscribe()\n{\n    _service->RequestSubscribeToPropertyUpdates(&_ctx,\n                                                &_notification_blocklist,\n                                                &_responder,\n                                                _async_rpc_queue,\n                                                _async_rpc_queue,\n                                                this);\n    _service->subscribe(this);\n}\n\nvoid SubscribeToPropertyUpdatesCallData::_unsubscribe()\n{\n    _service->unsubscribe(this);\n}\n\nbool SubscribeToPropertyUpdatesCallData::_check_if_blocklisted(const PropertyValue& reply)\n{\n    auto key = create_key(reply.property().property_id(), reply.property().processor_id());\n    return !(_blocklist.find(key) == _blocklist.end());\n}\n\nvoid SubscribeToPropertyUpdatesCallData::_populate_blocklist()\n{\n    for (auto& identifier : _notification_blocklist.properties())\n    {\n        _blocklist[create_key(identifier.property_id(), identifier.processor_id())] = false;\n    }\n}\nvoid SubscribeToAsyncCommandUpdatesCallData::_respawn()\n{\n    new SubscribeToAsyncCommandUpdatesCallData(_service, _async_rpc_queue);\n}\n\nvoid SubscribeToAsyncCommandUpdatesCallData::_subscribe()\n{\n    _service->RequestSubscribeToAsyncCommandUpdates(&_ctx,\n                                                    &_notification_blocklist,\n                                                    &_responder,\n                                                    _async_rpc_queue,\n                                                    _async_rpc_queue,\n                                                    this);\n    _service->subscribe(this);\n}\n\nvoid SubscribeToAsyncCommandUpdatesCallData::_unsubscribe()\n{\n    _service->unsubscribe(this);\n}\n\nbool SubscribeToAsyncCommandUpdatesCallData::_check_if_blocklisted([[maybe_unused]] const AsyncCommandResponse& reply)\n{\n    return false;\n}\n} // namespace sushi_rpc\n"
  },
  {
    "path": "rpc_interface/src/async_service_call_data.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Sushi Async gRPC Call Data. Objects to handle the async calls to sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_ASYNCSERVICECALLDATA_H\n#define SUSHI_ASYNCSERVICECALLDATA_H\n\n#include <grpc++/alarm.h>\n#include <grpcpp/grpcpp.h>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_UNUSED_PARAMETER\nELK_DISABLE_UNREACHABLE_CODE\n#include \"sushi_rpc.grpc.pb.h\"\nELK_POP_WARNING\n\n#include \"library/synchronised_fifo.h\"\n#include \"control_service.h\"\n\nnamespace sushi_rpc {\n\nusing BlocklistKey = int64_t;\n\nclass CallData\n{\npublic:\n    CallData(NotificationControlService* service,\n             grpc::ServerCompletionQueue* async_rpc_queue) : _service(service),\n                                                             _async_rpc_queue(async_rpc_queue),\n                                                             _in_completion_queue(false),\n                                                             _status(CallStatus::CREATE) {}\n\n    virtual ~CallData() = default;\n\n    virtual void proceed() = 0;\n\n    /**\n     * @brief Set the state of the call data to FINISH to make it destroy itself\n     *        on the next call to proceed.\n     */\n    void stop();\n\nprotected:\n    NotificationControlService* _service;\n    grpc::ServerCompletionQueue* _async_rpc_queue;\n    grpc::ServerContext _ctx;\n\n    grpc::Alarm _alarm;\n\n    bool _in_completion_queue;\n\n    enum class CallStatus\n    {\n        CREATE,\n        PROCESS,\n        PUSH_TO_BACK,\n        FINISH\n    };\n\n    CallStatus _status;\n\n    /**\n     * @brief Put the call data object at the back of the gRPC completion queue.\n     *        This will result in an error if this object is already placed in\n     *        the queue.\n     */\n    void _alert();\n};\n\ntemplate <class ValueType, class BlocklistType>\nclass SubscribeToUpdatesCallData : public CallData\n{\npublic:\n    SubscribeToUpdatesCallData(NotificationControlService* service,\n                               grpc::ServerCompletionQueue* async_rpc_queue)\n            : CallData(service, async_rpc_queue),\n              _responder(&_ctx)\n    {\n        // Classes inheriting from this, should call proceed() in their constructor.\n    }\n\n    void proceed() override;\n\n    void push(std::shared_ptr<ValueType> notification);\n\nprotected:\n    // Spawns a new CallData instance to serve new clients while we process\n    // the one for this CallData. The instance will deallocate itself as\n    // part of its FINISH state, in proceed().\n    virtual void _respawn() = 0;\n\n    virtual void _subscribe() = 0;\n    virtual void _unsubscribe() = 0;\n\n    virtual bool _check_if_blocklisted(const ValueType& reply) = 0;\n    virtual void _populate_blocklist() = 0;\n\n    BlocklistType _notification_blocklist;\n    grpc::ServerAsyncWriter<ValueType> _responder;\n\nprivate:\n    SynchronizedQueue<std::shared_ptr<ValueType>> _notifications;\n\n    bool _first_iteration{true};\n    bool _active{false};\n};\n\nclass SubscribeToTransportChangesCallData : public SubscribeToUpdatesCallData<TransportUpdate, GenericVoidValue>\n{\npublic:\n    SubscribeToTransportChangesCallData(NotificationControlService* service,\n                                        grpc::ServerCompletionQueue* async_rpc_queue)\n            : SubscribeToUpdatesCallData(service, async_rpc_queue)\n    {\n        proceed();\n    }\n\n    ~SubscribeToTransportChangesCallData() = default;\n\nprotected:\n    void _respawn() override;\n    void _subscribe() override;\n    void _unsubscribe() override;\n    bool _check_if_blocklisted(const TransportUpdate& reply) override;\n    void _populate_blocklist() override {}\n};\n\nclass SubscribeToCpuTimingUpdatesCallData : public SubscribeToUpdatesCallData<CpuTimings, GenericVoidValue>\n{\npublic:\n    SubscribeToCpuTimingUpdatesCallData(NotificationControlService* service,\n                                        grpc::ServerCompletionQueue* async_rpc_queue)\n            : SubscribeToUpdatesCallData(service, async_rpc_queue)\n    {\n        proceed();\n    }\n\n    ~SubscribeToCpuTimingUpdatesCallData() = default;\n\nprotected:\n    void _respawn() override;\n    void _subscribe() override;\n    void _unsubscribe() override;\n    bool _check_if_blocklisted(const CpuTimings& reply) override;\n    void _populate_blocklist() override {}\n};\n\nclass SubscribeToTrackChangesCallData : public SubscribeToUpdatesCallData<TrackUpdate, GenericVoidValue>\n{\npublic:\n    SubscribeToTrackChangesCallData(NotificationControlService* service,\n                                    grpc::ServerCompletionQueue* async_rpc_queue)\n            : SubscribeToUpdatesCallData(service, async_rpc_queue)\n    {\n        proceed();\n    }\n\n    ~SubscribeToTrackChangesCallData() = default;\n\nprotected:\n    void _respawn() override;\n    void _subscribe() override;\n    void _unsubscribe() override;\n    bool _check_if_blocklisted(const TrackUpdate& reply) override;\n    void _populate_blocklist() override {}\n};\n\nclass SubscribeToProcessorChangesCallData : public SubscribeToUpdatesCallData<ProcessorUpdate, GenericVoidValue>\n{\npublic:\n    SubscribeToProcessorChangesCallData(NotificationControlService* service,\n                                        grpc::ServerCompletionQueue* async_rpc_queue)\n            : SubscribeToUpdatesCallData(service, async_rpc_queue)\n    {\n        proceed();\n    }\n\n    ~SubscribeToProcessorChangesCallData() = default;\n\nprotected:\n    void _respawn() override;\n    void _subscribe() override;\n    void _unsubscribe() override;\n    bool _check_if_blocklisted(const ProcessorUpdate& reply) override;\n    void _populate_blocklist() override {}\n};\n\nclass SubscribeToParameterUpdatesCallData : public SubscribeToUpdatesCallData<ParameterUpdate, ParameterNotificationBlocklist>\n{\npublic:\n    SubscribeToParameterUpdatesCallData(NotificationControlService* service,\n                                        grpc::ServerCompletionQueue* async_rpc_queue)\n            : SubscribeToUpdatesCallData(service, async_rpc_queue)\n    {\n        proceed();\n    }\n\n    ~SubscribeToParameterUpdatesCallData() = default;\n\nprotected:\n    void _respawn() override;\n    void _subscribe() override;\n    void _unsubscribe() override;\n    bool _check_if_blocklisted(const ParameterUpdate& reply) override;\n    void _populate_blocklist() override;\n\nprivate:\n    std::unordered_map<BlocklistKey, bool> _blocklist;\n};\n\nclass SubscribeToPropertyUpdatesCallData : public SubscribeToUpdatesCallData<PropertyValue, PropertyNotificationBlocklist>\n{\npublic:\n    SubscribeToPropertyUpdatesCallData(NotificationControlService* service,\n                                       grpc::ServerCompletionQueue* async_rpc_queue)\n            : SubscribeToUpdatesCallData(service, async_rpc_queue)\n    {\n        proceed();\n    }\n\n    ~SubscribeToPropertyUpdatesCallData() = default;\n\nprotected:\n    void _respawn() override;\n    void _subscribe() override;\n    void _unsubscribe() override;\n    bool _check_if_blocklisted(const PropertyValue& reply) override;\n    void _populate_blocklist() override;\n\nprivate:\n    std::unordered_map<BlocklistKey, bool> _blocklist;\n};\n\nclass SubscribeToAsyncCommandUpdatesCallData : public SubscribeToUpdatesCallData<AsyncCommandResponse, GenericVoidValue>\n{\npublic:\n    SubscribeToAsyncCommandUpdatesCallData(NotificationControlService* service,\n                                           grpc::ServerCompletionQueue* async_rpc_queue)\n        : SubscribeToUpdatesCallData(service, async_rpc_queue)\n    {\n        proceed();\n    }\n\n    ~SubscribeToAsyncCommandUpdatesCallData() = default;\n\nprotected:\n    void _respawn() override;\n    void _subscribe() override;\n    void _unsubscribe() override;\n    bool _check_if_blocklisted(const AsyncCommandResponse& reply) override;\n    void _populate_blocklist() override {}\n};\n\n}\n#endif // SUSHI_ASYNCSERVICECALLDATA_H\n"
  },
  {
    "path": "rpc_interface/src/control_service.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Sushi Control Service, gRPC service for external control of Sushi\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"control_service.h\"\n\n#include \"sushi/control_notifications.h\"\n\n#include \"async_service_call_data.h\"\n\nnamespace sushi_rpc {\n\n/* Convenience conversion functions between sushi enums and their respective grpc implementations */\ninline sushi_rpc::ParameterType::Type to_grpc(const sushi::control::ParameterType type)\n{\n    switch (type)\n    {\n        case sushi::control::ParameterType::FLOAT:        return sushi_rpc::ParameterType::FLOAT;\n        case sushi::control::ParameterType::INT:          return sushi_rpc::ParameterType::INT;\n        case sushi::control::ParameterType::BOOL:         return sushi_rpc::ParameterType::BOOL;\n        default:                                          return sushi_rpc::ParameterType::FLOAT;\n    }\n}\n\ninline sushi_rpc::PlayingMode::Mode to_grpc(const sushi::control::PlayingMode mode)\n{\n    switch (mode)\n    {\n        case sushi::control::PlayingMode::STOPPED:      return sushi_rpc::PlayingMode::STOPPED;\n        case sushi::control::PlayingMode::PLAYING:      return sushi_rpc::PlayingMode::PLAYING;\n        case sushi::control::PlayingMode::RECORDING:    return sushi_rpc::PlayingMode::RECORDING;\n        default:                                        return sushi_rpc::PlayingMode::PLAYING;\n    }\n}\n\ninline MidiChannel_Channel to_grpc(const sushi::control::MidiChannel channel)\n{\n    switch (channel)\n    {\n        case sushi::control::MidiChannel::MIDI_CH_1:    return sushi_rpc::MidiChannel::MIDI_CH_1;\n        case sushi::control::MidiChannel::MIDI_CH_2:    return sushi_rpc::MidiChannel::MIDI_CH_2;\n        case sushi::control::MidiChannel::MIDI_CH_3:    return sushi_rpc::MidiChannel::MIDI_CH_3;\n        case sushi::control::MidiChannel::MIDI_CH_4:    return sushi_rpc::MidiChannel::MIDI_CH_4;\n        case sushi::control::MidiChannel::MIDI_CH_5:    return sushi_rpc::MidiChannel::MIDI_CH_5;\n        case sushi::control::MidiChannel::MIDI_CH_6:    return sushi_rpc::MidiChannel::MIDI_CH_6;\n        case sushi::control::MidiChannel::MIDI_CH_7:    return sushi_rpc::MidiChannel::MIDI_CH_7;\n        case sushi::control::MidiChannel::MIDI_CH_8:    return sushi_rpc::MidiChannel::MIDI_CH_8;\n        case sushi::control::MidiChannel::MIDI_CH_9:    return sushi_rpc::MidiChannel::MIDI_CH_9;\n        case sushi::control::MidiChannel::MIDI_CH_10:   return sushi_rpc::MidiChannel::MIDI_CH_10;\n        case sushi::control::MidiChannel::MIDI_CH_11:   return sushi_rpc::MidiChannel::MIDI_CH_11;\n        case sushi::control::MidiChannel::MIDI_CH_12:   return sushi_rpc::MidiChannel::MIDI_CH_12;\n        case sushi::control::MidiChannel::MIDI_CH_13:   return sushi_rpc::MidiChannel::MIDI_CH_13;\n        case sushi::control::MidiChannel::MIDI_CH_14:   return sushi_rpc::MidiChannel::MIDI_CH_14;\n        case sushi::control::MidiChannel::MIDI_CH_15:   return sushi_rpc::MidiChannel::MIDI_CH_15;\n        case sushi::control::MidiChannel::MIDI_CH_16:   return sushi_rpc::MidiChannel::MIDI_CH_16;\n        case sushi::control::MidiChannel::MIDI_CH_OMNI: return sushi_rpc::MidiChannel::MIDI_CH_OMNI;\n        default:                                        return sushi_rpc::MidiChannel::MIDI_CH_OMNI;\n    }\n}\n\ninline sushi::control::MidiChannel to_sushi_ext(const MidiChannel_Channel channel)\n{\n    switch (channel)\n    {\n        case sushi_rpc::MidiChannel::MIDI_CH_1:    return sushi::control::MidiChannel::MIDI_CH_1;\n        case sushi_rpc::MidiChannel::MIDI_CH_2:    return sushi::control::MidiChannel::MIDI_CH_2;\n        case sushi_rpc::MidiChannel::MIDI_CH_3:    return sushi::control::MidiChannel::MIDI_CH_3;\n        case sushi_rpc::MidiChannel::MIDI_CH_4:    return sushi::control::MidiChannel::MIDI_CH_4;\n        case sushi_rpc::MidiChannel::MIDI_CH_5:    return sushi::control::MidiChannel::MIDI_CH_5;\n        case sushi_rpc::MidiChannel::MIDI_CH_6:    return sushi::control::MidiChannel::MIDI_CH_6;\n        case sushi_rpc::MidiChannel::MIDI_CH_7:    return sushi::control::MidiChannel::MIDI_CH_7;\n        case sushi_rpc::MidiChannel::MIDI_CH_8:    return sushi::control::MidiChannel::MIDI_CH_8;\n        case sushi_rpc::MidiChannel::MIDI_CH_9:    return sushi::control::MidiChannel::MIDI_CH_9;\n        case sushi_rpc::MidiChannel::MIDI_CH_10:   return sushi::control::MidiChannel::MIDI_CH_10;\n        case sushi_rpc::MidiChannel::MIDI_CH_11:   return sushi::control::MidiChannel::MIDI_CH_11;\n        case sushi_rpc::MidiChannel::MIDI_CH_12:   return sushi::control::MidiChannel::MIDI_CH_12;\n        case sushi_rpc::MidiChannel::MIDI_CH_13:   return sushi::control::MidiChannel::MIDI_CH_13;\n        case sushi_rpc::MidiChannel::MIDI_CH_14:   return sushi::control::MidiChannel::MIDI_CH_14;\n        case sushi_rpc::MidiChannel::MIDI_CH_15:   return sushi::control::MidiChannel::MIDI_CH_15;\n        case sushi_rpc::MidiChannel::MIDI_CH_16:   return sushi::control::MidiChannel::MIDI_CH_16;\n        case sushi_rpc::MidiChannel::MIDI_CH_OMNI: return sushi::control::MidiChannel::MIDI_CH_OMNI;\n        default:                                   return sushi::control::MidiChannel::MIDI_CH_OMNI;\n    }\n}\n\ninline sushi::control::PlayingMode to_sushi_ext(const sushi_rpc::PlayingMode::Mode mode)\n{\n    switch (mode)\n    {\n        case sushi_rpc::PlayingMode::STOPPED:   return sushi::control::PlayingMode::STOPPED;\n        case sushi_rpc::PlayingMode::PLAYING:   return sushi::control::PlayingMode::PLAYING;\n        case sushi_rpc::PlayingMode::RECORDING: return sushi::control::PlayingMode::RECORDING;\n        default:                                return sushi::control::PlayingMode::PLAYING;\n    }\n}\n\ninline sushi_rpc::SyncMode::Mode to_grpc(const sushi::control::SyncMode mode)\n{\n    switch (mode)\n    {\n        case sushi::control::SyncMode::INTERNAL: return sushi_rpc::SyncMode::INTERNAL;\n        case sushi::control::SyncMode::MIDI:     return sushi_rpc::SyncMode::MIDI;\n        case sushi::control::SyncMode::LINK:     return sushi_rpc::SyncMode::LINK;\n        default:                                 return sushi_rpc::SyncMode::INTERNAL;\n    }\n}\n\ninline sushi::control::SyncMode to_sushi_ext(const sushi_rpc::SyncMode::Mode mode)\n{\n    switch (mode)\n    {\n        case sushi_rpc::SyncMode::INTERNAL: return sushi::control::SyncMode::INTERNAL;\n        case sushi_rpc::SyncMode::MIDI:     return sushi::control::SyncMode::MIDI;\n        case sushi_rpc::SyncMode::LINK:     return sushi::control::SyncMode::LINK;\n        default:                            return sushi::control::SyncMode::INTERNAL;\n    }\n}\n\ninline sushi_rpc::TrackType::Type to_grpc(const sushi::control::TrackType type)\n{\n    switch (type)\n    {\n        case sushi::control::TrackType::REGULAR:  return sushi_rpc::TrackType::REGULAR;\n        case sushi::control::TrackType::PRE:      return sushi_rpc::TrackType::PRE;\n        case sushi::control::TrackType::POST:     return sushi_rpc::TrackType::POST;\n        default:                                  return sushi_rpc::TrackType::REGULAR;\n    }\n}\n\ninline sushi::control::TrackType to_sushi_ext(const sushi_rpc::TrackType::Type type)\n{\n    switch (type)\n    {\n        case sushi_rpc::TrackType::REGULAR: return sushi::control::TrackType::REGULAR;\n        case sushi_rpc::TrackType::PRE:     return sushi::control::TrackType::PRE;\n        case sushi_rpc::TrackType::POST:    return sushi::control::TrackType::POST;\n        default:                            return sushi::control::TrackType::REGULAR;\n    }\n}\n\ninline const char* to_string(const sushi::control::ControlStatus status)\n{\n   switch (status)\n    {\n        case sushi::control::ControlStatus::OK:                    return \"OK\";\n        case sushi::control::ControlStatus::ERROR:                 return \"ERROR\";\n        case sushi::control::ControlStatus::UNSUPPORTED_OPERATION: return \"UNSUPPORTED OPERATION\";\n        case sushi::control::ControlStatus::NOT_FOUND:             return \"NOT FOUND\";\n        case sushi::control::ControlStatus::OUT_OF_RANGE:          return \"OUT OF RANGE\";\n        case sushi::control::ControlStatus::INVALID_ARGUMENTS:     return \"INVALID ARGUMENTS\";\n        default:                                                   return \"INTERNAL\";\n    }\n}\n\ninline grpc::Status to_grpc_status(sushi::control::ControlStatus status, const char* error = nullptr)\n{\n    if (!error)\n    {\n        error = to_string(status);\n    }\n    switch (status)\n    {\n        case sushi::control::ControlStatus::OK:\n            return ::grpc::Status::OK;\n\n        case sushi::control::ControlStatus::ERROR:\n            return ::grpc::Status(::grpc::StatusCode::UNKNOWN, error);\n\n        case sushi::control::ControlStatus::UNSUPPORTED_OPERATION:\n            return ::grpc::Status(::grpc::StatusCode::FAILED_PRECONDITION, error);\n\n        case sushi::control::ControlStatus::NOT_FOUND:\n            return ::grpc::Status(::grpc::StatusCode::NOT_FOUND, error);\n\n        case sushi::control::ControlStatus::OUT_OF_RANGE:\n            return ::grpc::Status(::grpc::StatusCode::OUT_OF_RANGE, error);\n\n        case sushi::control::ControlStatus::INVALID_ARGUMENTS:\n            return ::grpc::Status(::grpc::StatusCode::INVALID_ARGUMENT, error);\n\n        default:\n            return ::grpc::Status(::grpc::StatusCode::INTERNAL, error);\n    }\n}\n\ninline sushi_rpc::CommandStatus::Status to_grpc(sushi::control::ControlStatus status)\n{\n    switch (status)\n    {\n        case sushi::control::ControlStatus::OK:                    return sushi_rpc::CommandStatus::SUCCESS;\n        case sushi::control::ControlStatus::ASYNC_RESPONSE:        return sushi_rpc::CommandStatus::ASYNC_RESPONSE;\n        case sushi::control::ControlStatus::ERROR:                 return sushi_rpc::CommandStatus::ERROR;\n        case sushi::control::ControlStatus::UNSUPPORTED_OPERATION: return sushi_rpc::CommandStatus::UNSUPPORTED_OPERATION;\n        case sushi::control::ControlStatus::NOT_FOUND:             return sushi_rpc::CommandStatus::NOT_FOUND;\n        case sushi::control::ControlStatus::OUT_OF_RANGE:          return sushi_rpc::CommandStatus::OUT_OF_RANGE;\n        case sushi::control::ControlStatus::INVALID_ARGUMENTS:     return sushi_rpc::CommandStatus::INVALID_ARGUMENTS;\n        default:                                                   return sushi_rpc::CommandStatus::DUMMY;\n    }\n}\n\ninline void to_grpc(CommandResponse& dest, const sushi::control::ControlResponse src)\n{\n    dest.mutable_status()->set_status(to_grpc(src.status));\n    dest.set_id(src.id);\n}\n\ninline void to_grpc(CommandResponse& dest, const sushi::control::ControlStatus src)\n{\n    dest.mutable_status()->set_status(to_grpc(src));\n    dest.set_id(0);\n}\n\ninline void to_grpc(ParameterInfo& dest, const sushi::control::ParameterInfo& src)\n{\n    dest.set_id(src.id);\n    dest.mutable_type()->set_type(to_grpc(src.type));\n    dest.set_label(src.label);\n    dest.set_name(src.name);\n    dest.set_unit(src.unit);\n    dest.set_automatable(src.automatable);\n    dest.set_min_domain_value(src.min_domain_value);\n    dest.set_max_domain_value(src.max_domain_value);\n}\n\n//inline void to_grpc(ParameterIdentifier& dest, const sushi::control::ParameterChangeNotifica\n\ninline void to_grpc(PropertyInfo& dest, const sushi::control::PropertyInfo& src)\n{\n    dest.set_id(src.id);\n    dest.set_name(src.name);\n    dest.set_label(src.label);\n}\n\ninline void to_grpc(sushi_rpc::ProcessorInfo& dest, const sushi::control::ProcessorInfo& src)\n{\n    dest.set_id(src.id);\n    dest.set_label(src.label);\n    dest.set_name(src.name);\n    dest.set_parameter_count(src.parameter_count);\n    dest.set_program_count(src.program_count);\n}\n\ninline void to_grpc(sushi_rpc::MidiKbdConnection& dest, const sushi::control::MidiKbdConnection& src)\n{\n    dest.mutable_track()->set_id(src.track_id);\n    dest.mutable_channel()->set_channel(to_grpc(src.channel));\n    dest.set_port(src.port);\n    dest.set_raw_midi(src.raw_midi);\n}\n\ninline void to_grpc(sushi_rpc::MidiCCConnection& dest, const sushi::control::MidiCCConnection& src)\n{\n    dest.mutable_parameter()->set_processor_id(src.processor_id);\n    dest.mutable_parameter()->set_parameter_id(src.parameter_id);\n    dest.mutable_parameter()->set_processor_id(src.processor_id);\n    dest.mutable_channel()->set_channel(to_grpc(src.channel));\n    dest.set_port(src.port);\n    dest.set_cc_number(src.cc_number);\n    dest.set_min_range(static_cast<float>(src.min_range));\n    dest.set_max_range(static_cast<float>(src.max_range));\n    dest.set_relative_mode(src.relative_mode);\n}\n\ninline void to_grpc(sushi_rpc::MidiPCConnection& dest, const sushi::control::MidiPCConnection& src)\n{\n    dest.mutable_processor()->set_id(src.processor_id);\n    dest.mutable_channel()->set_channel(to_grpc(src.channel));\n    dest.set_port(src.port);\n}\n\ninline void to_grpc(sushi_rpc::TrackInfo& dest, const sushi::control::TrackInfo& src)\n{\n    dest.set_id(src.id);\n    dest.set_label(src.label);\n    dest.set_name(src.name);\n    dest.set_channels(src.channels);\n    dest.set_buses(src.buses);\n    dest.set_thread(src.thread);\n    dest.mutable_type()->set_type(to_grpc(src.type));\n    for (auto i : src.processors)\n    {\n        dest.mutable_processors()->Add()->set_id(i);\n    }\n}\n\ninline void to_grpc(sushi_rpc::Timings& dest, const sushi::control::Timings& src)\n{\n    dest.set_average(src.avg);\n    dest.set_min(src.min);\n    dest.set_max(src.max);\n}\n\ninline void to_grpc(sushi_rpc::CpuTimings& dest, const sushi::control::CpuTimings& src)\n{\n    to_grpc(*dest.mutable_main(), src.main);\n    for (const auto& thread : src.threads)\n    {\n        to_grpc(*dest.mutable_threads()->Add(), thread);\n    }\n}\n\ninline void to_grpc(sushi_rpc::AudioConnection& dest, const sushi::control::AudioConnection& src)\n{\n    dest.mutable_track()->set_id(src.track_id);\n    dest.set_track_channel(src.track_channel);\n    dest.set_engine_channel(src.engine_channel);\n}\n\ninline sushi_rpc::PluginType::Type to_grpc(const sushi::control::PluginType type)\n{\n    switch (type)\n    {\n        case sushi::control::PluginType::INTERNAL:       return sushi_rpc::PluginType::INTERNAL;\n        case sushi::control::PluginType::VST2X:          return sushi_rpc::PluginType::VST2X;\n        case sushi::control::PluginType::VST3X:          return sushi_rpc::PluginType::VST3X;\n        case sushi::control::PluginType::LV2:            return sushi_rpc::PluginType::LV2;\n        default:                                         return sushi_rpc::PluginType::INTERNAL;\n    }\n}\n\ninline sushi::control::PluginType to_sushi_ext(const sushi_rpc::PluginType::Type type)\n{\n    switch (type)\n    {\n        case sushi_rpc::PluginType::INTERNAL:       return sushi::control::PluginType::INTERNAL;\n        case sushi_rpc::PluginType::VST2X:          return sushi::control::PluginType::VST2X;\n        case sushi_rpc::PluginType::VST3X:          return sushi::control::PluginType::VST3X;\n        case sushi_rpc::PluginType::LV2:            return sushi::control::PluginType::LV2;\n        default:                                    return sushi::control::PluginType::INTERNAL;\n    }\n}\n\ninline void to_grpc(sushi_rpc::ProcessorState& dest, sushi::control::ProcessorState& src)\n{\n    if (src.program.has_value())\n    {\n        dest.mutable_program_id()->set_value(src.program.value());\n        dest.mutable_program_id()->set_has_value(true);\n    }\n    if (src.bypassed.has_value())\n    {\n        dest.mutable_bypassed()->set_value(src.bypassed.value());\n        dest.mutable_bypassed()->set_has_value(true);\n    }\n\n    dest.mutable_properties()->Reserve(static_cast<int>(src.properties.size()));\n    for (auto& p : src.properties)\n    {\n        auto target = dest.mutable_properties()->Add();\n        target->mutable_property()->set_property_id(p.first);\n        target->set_value(std::move(p.second));\n    }\n\n    dest.mutable_parameters()->Reserve(static_cast<int>(src.parameters.size()));\n    for (const auto& p : src.parameters)\n    {\n        auto target = dest.mutable_parameters()->Add();\n        target->mutable_parameter()->set_parameter_id(p.first);\n        target->set_value(p.second);\n    }\n\n    // Todo: investigate if this can be moved for efficiency\n    dest.mutable_binary_data()->append(reinterpret_cast<const char*>(src.binary_data.data()),\n                                       reinterpret_cast<const char*>(src.binary_data.data()) + src.binary_data.size());\n}\n\ninline void to_sushi_ext(sushi::control::ProcessorState& dest, const sushi_rpc::ProcessorState& src)\n{\n    if (src.program_id().has_value())\n    {\n        dest.program = src.program_id().value();\n    }\n    if (src.bypassed().has_value())\n    {\n        dest.bypassed = src.bypassed().value();\n    }\n\n    dest.properties.reserve(src.properties_size());\n    for (const auto& p : src.properties())\n    {\n        dest.properties.push_back({p.property().property_id(), p.value()});\n    }\n\n    dest.parameters.reserve(src.parameters_size());\n    for (const auto& p : src.parameters())\n    {\n        dest.parameters.push_back({p.parameter().parameter_id(), p.value()});\n    }\n\n    dest.binary_data.reserve(src.binary_data().size());\n    dest.binary_data.insert(dest.binary_data.begin(),\n                            reinterpret_cast<const std::byte*>(src.binary_data().data()),\n                            reinterpret_cast<const std::byte*>(src.binary_data().data()) + src.binary_data().size());\n}\n\ninline void to_grpc(sushi_rpc::SushiBuildInfo& dest, sushi::control::SushiBuildInfo& src)\n{\n    dest.set_version(std::move(src.version));\n\n    for (auto& option : src.build_options)\n    {\n        dest.add_build_options(std::move(option));\n    }\n\n    dest.set_audio_buffer_size(src.audio_buffer_size);\n    dest.set_commit_hash(std::move(src.commit_hash));\n    dest.set_build_date(std::move(src.build_date));\n}\n\ninline void to_sushi_ext(sushi::control::SushiBuildInfo& dest, const sushi_rpc::SushiBuildInfo& src)\n{\n    dest.version = src.version();\n\n    for (auto& option : src.build_options())\n    {\n        dest.build_options.push_back(option);\n    }\n\n    dest.audio_buffer_size = src.audio_buffer_size();\n    dest.commit_hash = src.commit_hash();\n    dest.build_date = src.build_date();\n}\n\ninline void to_grpc(sushi_rpc::OscParameterState& dest, sushi::control::OscParameterState& src)\n{\n    dest.set_processor(std::move(src.processor));\n    dest.mutable_parameter_ids()->Reserve(static_cast<int>(src.parameter_ids.size()));\n    for (const auto& id : src.parameter_ids)\n    {\n        dest.mutable_parameter_ids()->Add(id);\n    }\n}\n\ninline sushi::control::OscParameterState to_sushi_ext(const sushi_rpc::OscParameterState& src)\n{\n    sushi::control::OscParameterState dest;\n    dest.processor = src.processor();\n    dest.parameter_ids.insert(dest.parameter_ids.begin(), src.parameter_ids().begin(), src.parameter_ids().end());\n    return dest;\n}\n\ninline void to_grpc(sushi_rpc::OscState& dest, sushi::control::OscState& src)\n{\n    dest.set_enable_all_processor_outputs(src.enable_all_processor_outputs);\n    dest.mutable_enabled_processor_outputs()->Reserve(static_cast<int>(src.enabled_processor_outputs.size()));\n    for (auto& state : src.enabled_processor_outputs)\n    {\n        auto grpc_state = dest.mutable_enabled_processor_outputs()->Add();\n        to_grpc(*grpc_state, state);\n    }\n}\n\ninline void to_sushi_ext(sushi::control::OscState& dest, const sushi_rpc::OscState& src)\n{\n    dest.enable_all_processor_outputs = src.enable_all_processor_outputs();\n    dest.enabled_processor_outputs.reserve(src.enabled_processor_outputs_size());\n    for (auto& state : src.enabled_processor_outputs())\n    {\n        dest.enabled_processor_outputs.push_back(to_sushi_ext(state));\n    }\n}\n\ninline void to_grpc(sushi_rpc::MidiKbdConnectionState& dest, sushi::control::MidiKbdConnectionState& src)\n{\n    dest.set_track(std::move(src.track));\n    dest.mutable_channel()->set_channel(to_grpc(src.channel));\n    dest.set_port(src.port);\n    dest.set_raw_midi(src.raw_midi);\n}\n\ninline sushi::control::MidiKbdConnectionState to_sushi_ext(const sushi_rpc::MidiKbdConnectionState& src)\n{\n    sushi::control::MidiKbdConnectionState dest;\n    dest.track = src.track();\n    dest.channel = to_sushi_ext(src.channel().channel());\n    dest.port = src.port();\n    dest.raw_midi = src.raw_midi();\n    return dest;\n}\n\ninline void to_grpc(sushi_rpc::MidiCCConnectionState& dest, sushi::control::MidiCCConnectionState& src)\n{\n    dest.set_processor(std::move(src.processor));\n    dest.mutable_parameter()->set_parameter_id(src.parameter_id);\n    dest.mutable_channel()->set_channel(to_grpc(src.channel));\n    dest.set_port(src.port);\n    dest.set_cc_number(src.cc_number);\n    dest.set_min_range(src.min_range);\n    dest.set_max_range(src.max_range);\n    dest.set_relative_mode(src.relative_mode);\n}\n\ninline sushi::control::MidiCCConnectionState to_sushi_ext(const sushi_rpc::MidiCCConnectionState& src)\n{\n    sushi::control::MidiCCConnectionState dest;\n    dest.processor = src.processor();\n    dest.channel = to_sushi_ext(src.channel().channel());\n    dest.port = src.port();\n    dest.cc_number = src.cc_number();\n    dest.min_range = src.min_range();\n    dest.max_range = src.max_range();\n    dest.relative_mode = src.relative_mode();\n    return dest;\n}\n\ninline void to_grpc(sushi_rpc::MidiPCConnectionState& dest, sushi::control::MidiPCConnectionState& src)\n{\n    dest.set_processor(std::move(src.processor));\n    dest.mutable_channel()->set_channel(to_grpc(src.channel));\n    dest.set_port(src.port);\n}\n\ninline sushi::control::MidiPCConnectionState to_sushi_ext(const sushi_rpc::MidiPCConnectionState& src)\n{\n    sushi::control::MidiPCConnectionState dest;\n    dest.processor = src.processor();\n    dest.channel = to_sushi_ext(src.channel().channel());\n    dest.port = src.port();\n    return dest;\n}\n\ninline void to_grpc(sushi_rpc::MidiState& dest, sushi::control::MidiState& src)\n{\n    dest.set_inputs(src.inputs);\n    dest.set_outputs(src.outputs);\n\n    dest.mutable_kbd_input_connections()->Reserve(static_cast<int>(src.kbd_input_connections.size()));\n    for (auto& con : src.kbd_input_connections)\n    {\n        auto grpc_con = dest.mutable_kbd_input_connections()->Add();\n        to_grpc(*grpc_con, con);\n    }\n\n    dest.mutable_kbd_output_connections()->Reserve(static_cast<int>(src.kbd_output_connections.size()));\n    for (auto& con : src.kbd_output_connections)\n    {\n        auto grpc_con = dest.mutable_kbd_output_connections()->Add();\n        to_grpc(*grpc_con, con);\n    }\n\n    dest.mutable_cc_connections()->Reserve(static_cast<int>(src.cc_connections.size()));\n    for (auto& con : src.cc_connections)\n    {\n        auto grpc_con = dest.mutable_cc_connections()->Add();\n        to_grpc(*grpc_con, con);\n    }\n\n    dest.mutable_pc_connections()->Reserve(static_cast<int>(src.pc_connections.size()));\n    for (auto& con : src.pc_connections)\n    {\n        auto grpc_con = dest.mutable_pc_connections()->Add();\n        to_grpc(*grpc_con, con);\n    }\n\n    dest.mutable_enabled_clock_outputs()->Reserve(static_cast<int>(src.enabled_clock_outputs.size()));\n    for (auto port : src.enabled_clock_outputs)\n    {\n        dest.mutable_enabled_clock_outputs()->Add(port);\n    }\n}\n\ninline void to_sushi_ext(sushi::control::MidiState& dest, const sushi_rpc::MidiState& src)\n{\n    dest.inputs = src.inputs();\n    dest.outputs = src.outputs();\n\n    dest.kbd_input_connections.reserve(src.kbd_input_connections_size());\n    for (auto& con : src.kbd_input_connections())\n    {\n        dest.kbd_input_connections.push_back(to_sushi_ext(con));\n    }\n\n    dest.kbd_output_connections.reserve(src.kbd_output_connections_size());\n    for (auto& con : src.kbd_output_connections())\n    {\n        dest.kbd_output_connections.push_back(to_sushi_ext(con));\n    }\n\n    dest.cc_connections.reserve(src.cc_connections_size());\n    for (auto& con : src.cc_connections())\n    {\n        dest.cc_connections.push_back(to_sushi_ext(con));\n    }\n    dest.pc_connections.reserve(src.pc_connections_size());\n    for (auto& con : src.pc_connections())\n    {\n        dest.pc_connections.push_back(to_sushi_ext(con));\n    }\n    dest.enabled_clock_outputs = std::vector<int>(src.enabled_clock_outputs().begin(), src.enabled_clock_outputs().end());\n}\n\ninline void to_grpc(sushi_rpc::TrackAudioConnectionState& dest, sushi::control::TrackAudioConnectionState& src)\n{\n    dest.set_track(std::move(src.track));\n    dest.set_track_channel(src.track_channel);\n    dest.set_engine_channel(src.engine_channel);\n}\n\ninline sushi::control::TrackAudioConnectionState to_sushi_ext(const sushi_rpc::TrackAudioConnectionState& src)\n{\n    sushi::control::TrackAudioConnectionState dest;\n    dest.track = src.track();\n    dest.track_channel = src.track_channel();\n    dest.engine_channel = src.engine_channel();\n    return dest;\n}\n\ninline void to_grpc(sushi_rpc::EngineState& dest, sushi::control::EngineState& src)\n{\n    dest.set_sample_rate(src.sample_rate);\n    dest.set_tempo(src.tempo);\n    dest.mutable_playing_mode()->set_mode(to_grpc(src.playing_mode));\n    dest.mutable_sync_mode()->set_mode(to_grpc(src.sync_mode));\n    dest.mutable_time_signature()->set_denominator(src.time_signature.denominator);\n    dest.mutable_time_signature()->set_numerator(src.time_signature.numerator);\n    dest.set_clip_detection_input(src.input_clip_detection);\n    dest.set_clip_detection_output(src.output_clip_detection);\n    dest.set_master_limiter(src.master_limiter);\n    dest.set_used_audio_inputs(src.used_audio_inputs);\n    dest.set_used_audio_outputs(src.used_audio_outputs);\n\n    dest.mutable_input_connections()->Reserve(static_cast<int>(src.input_connections.size()));\n    for (auto& con : src.input_connections)\n    {\n        auto grpc_con = dest.mutable_input_connections()->Add();\n        to_grpc(*grpc_con, con);\n    }\n\n    dest.mutable_output_connections()->Reserve(static_cast<int>(src.output_connections.size()));\n    for (auto& con : src.output_connections)\n    {\n        auto grpc_con = dest.mutable_output_connections()->Add();\n        to_grpc(*grpc_con, con);\n    }\n}\n\ninline void to_sushi_ext(sushi::control::EngineState& dest, const sushi_rpc::EngineState& src)\n{\n    dest.sample_rate = src.sample_rate();\n    dest.tempo = src.tempo();\n    dest.playing_mode = to_sushi_ext(src.playing_mode().mode());\n    dest.sync_mode = to_sushi_ext(src.sync_mode().mode());\n    dest.time_signature = {src.time_signature().numerator(), src.time_signature().denominator()};\n    dest.input_clip_detection = src.clip_detection_input();\n    dest.output_clip_detection = src.clip_detection_output();\n    dest.master_limiter = src.master_limiter();\n    dest.used_audio_inputs = src.used_audio_inputs();\n    dest.used_audio_outputs = src.used_audio_outputs();\n\n    dest.input_connections.reserve(src.input_connections_size());\n    for (auto& con : src.input_connections())\n    {\n        dest.input_connections.push_back(to_sushi_ext(con));\n    }\n\n    dest.output_connections.reserve(src.output_connections_size());\n    for (auto& con : src.output_connections())\n    {\n        dest.output_connections.push_back(to_sushi_ext(con));\n    }\n}\n\ninline void to_grpc(sushi_rpc::PluginClass& dest, sushi::control::PluginClass& src)\n{\n    dest.set_name(std::move(src.name));\n    dest.set_label(std::move(src.label));\n    dest.set_uid(std::move(src.uid));\n    dest.set_path(std::move(src.path));\n    dest.mutable_type()->set_type(to_grpc(src.type));\n    to_grpc(*dest.mutable_state(), src.state);\n}\n\ninline sushi::control::PluginClass to_sushi_ext(const sushi_rpc::PluginClass& src)\n{\n    sushi::control::PluginClass dest;\n    dest.name = src.name();\n    dest.label = src.label();\n    dest.uid = src.uid();\n    dest.path = src.path();\n    dest.type = to_sushi_ext(src.type().type());\n    to_sushi_ext(dest.state, src.state());\n    return dest;\n}\n\ninline void to_grpc(sushi_rpc::TrackState& dest, sushi::control::TrackState& src)\n{\n    dest.set_name(std::move(src.name));\n    dest.set_label(std::move(src.label));\n    dest.set_channels(src.channels);\n    dest.set_buses(src.buses);\n    dest.set_thread(src.thread);\n    dest.mutable_type()->set_type(to_grpc(src.type));\n    to_grpc(*dest.mutable_track_state(), src.track_state);\n\n    dest.mutable_processors()->Reserve(static_cast<int>(src.processors.size()));\n    for (auto& proc : src.processors)\n    {\n        auto grpc_proc = dest.mutable_processors()->Add();\n        to_grpc(*grpc_proc, proc);\n    }\n}\n\ninline sushi::control::TrackState to_sushi_ext(const sushi_rpc::TrackState& src)\n{\n    sushi::control::TrackState dest;\n    dest.name = src.name();\n    dest.label = src.label();\n    dest.channels = src.channels();\n    dest.buses = src.buses();\n    dest.type = to_sushi_ext(src.type().type());\n    to_sushi_ext(dest.track_state, src.track_state());\n\n    dest.processors.reserve(src.processors_size());\n    for (const auto& processor : src.processors())\n    {\n        dest.processors.push_back(to_sushi_ext(processor));\n    }\n    return dest;\n}\n\ninline void to_grpc(sushi_rpc::SessionState& dest, sushi::control::SessionState& src)\n{\n    to_grpc(*dest.mutable_sushi_info(), src.sushi_info);\n    dest.set_save_date(std::move(src.save_date));\n    to_grpc(*dest.mutable_osc_state(), src.osc_state);\n    to_grpc(*dest.mutable_midi_state(), src.midi_state);\n    to_grpc(*dest.mutable_engine_state(), src.engine_state);\n\n    dest.mutable_tracks()->Reserve(static_cast<int>(src.tracks.size()));\n    for (auto& track : src.tracks)\n    {\n        auto grpc_track = dest.mutable_tracks()->Add();\n        to_grpc(*grpc_track, track);\n    }\n}\n\ninline void to_sushi_ext(sushi::control::SessionState& dest, const sushi_rpc::SessionState& src)\n{\n    to_sushi_ext(dest.sushi_info, src.sushi_info());\n    dest.save_date = src.save_date();\n    to_sushi_ext(dest.osc_state, src.osc_state());\n    to_sushi_ext(dest.midi_state, src.midi_state());\n    to_sushi_ext(dest.engine_state, src.engine_state());\n\n    dest.tracks.reserve(src.tracks_size());\n    for (auto& track : src.tracks())\n    {\n        dest.tracks.push_back(to_sushi_ext(track));\n    }\n}\n\ngrpc::Status SystemControlService::GetSushiVersion(grpc::ServerContext* /*context*/,\n                                                   const sushi_rpc::GenericVoidValue* /*request*/,\n                                                   sushi_rpc::GenericStringValue* response)\n{\n    response->set_value(_controller->get_sushi_version());\n    return grpc::Status::OK;\n}\n\ngrpc::Status SystemControlService::GetSushiApiVersion(grpc::ServerContext* /*context*/,\n                                                      const sushi_rpc::GenericVoidValue* /*request*/,\n                                                      sushi_rpc::GenericStringValue* response)\n{\n    response->set_value(_controller->get_sushi_api_version());\n    return grpc::Status::OK;\n}\n\ngrpc::Status SystemControlService::GetBuildInfo(grpc::ServerContext* /*context*/,\n                                                const sushi_rpc::GenericVoidValue* /*request*/,\n                                                sushi_rpc::SushiBuildInfo* response)\n{\n    auto build_info = _controller->get_sushi_build_info();\n    to_grpc(*response, build_info);\n    return grpc::Status::OK;\n}\n\ngrpc::Status SystemControlService::GetInputAudioChannelCount(grpc::ServerContext* /*context*/,\n                                                             const sushi_rpc::GenericVoidValue* /*request*/,\n                                                             sushi_rpc::GenericIntValue* response)\n{\n    response->set_value(_controller->get_input_audio_channel_count());\n    return grpc::Status::OK;\n}\n\ngrpc::Status SystemControlService::GetOutputAudioChannelCount(grpc::ServerContext* /*context*/,\n                                                              const sushi_rpc::GenericVoidValue* /*request*/,\n                                                              sushi_rpc::GenericIntValue* response)\n{\n    response->set_value(_controller->get_output_audio_channel_count());\n    return grpc::Status::OK;\n}\n\ngrpc::Status TransportControlService::GetSamplerate(grpc::ServerContext* /*context*/,\n                                                    const sushi_rpc::GenericVoidValue* /*request*/,\n                                                    sushi_rpc::GenericFloatValue* response)\n{\n    response->set_value(_controller->get_samplerate());\n    return grpc::Status::OK;\n}\n\ngrpc::Status TransportControlService::GetPlayingMode(grpc::ServerContext* /*context*/,\n                                                     const sushi_rpc::GenericVoidValue* /*request*/,\n                                                     sushi_rpc::PlayingMode* response)\n{\n    response->set_mode(to_grpc(_controller->get_playing_mode()));\n    return grpc::Status::OK;\n}\n\ngrpc::Status TransportControlService::GetSyncMode(grpc::ServerContext* /*context*/,\n                                                  const sushi_rpc::GenericVoidValue* /*request*/,\n                                                  sushi_rpc::SyncMode* response)\n{\n    response->set_mode(to_grpc(_controller->get_sync_mode()));\n    return grpc::Status::OK;\n}\n\ngrpc::Status TransportControlService::GetTimeSignature(grpc::ServerContext* /*context*/,\n                                                       const sushi_rpc::GenericVoidValue* /*request*/,\n                                                       sushi_rpc::TimeSignature* response)\n{\n    auto ts = _controller->get_time_signature();\n    response->set_denominator(ts.denominator);\n    response->set_numerator(ts.numerator);\n    return grpc::Status::OK;\n}\n\ngrpc::Status TransportControlService::GetTempo(grpc::ServerContext* /*context*/,\n                                               const sushi_rpc::GenericVoidValue* /*request*/,\n                                               sushi_rpc::GenericFloatValue* response)\n{\n    response->set_value(_controller->get_tempo());\n    return grpc::Status::OK;\n}\n\ngrpc::Status TransportControlService::SetTempo(grpc::ServerContext* /*context*/,\n                                               const sushi_rpc::GenericFloatValue* request,\n                                               sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->set_tempo(request->value());\n    to_grpc(*response, status);\n    return grpc::Status::OK;}\n\ngrpc::Status TransportControlService::SetPlayingMode(grpc::ServerContext* /*context*/,\n                                                     const sushi_rpc::PlayingMode* request,\n                                                     sushi_rpc::CommandResponse* response)\n{\n    _controller->set_playing_mode(to_sushi_ext(request->mode()));\n    response->mutable_status()->set_status(CommandStatus::SUCCESS);\n    return grpc::Status::OK;\n}\n\ngrpc::Status TransportControlService::SetSyncMode(grpc::ServerContext* /*context*/,\n                                                  const sushi_rpc::SyncMode*request,\n                                                  sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->set_sync_mode(to_sushi_ext(request->mode()));\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status TransportControlService::SetTimeSignature(grpc::ServerContext* /*context*/,\n                                                       const sushi_rpc::TimeSignature* request,\n                                                       sushi_rpc::CommandResponse* response)\n{\n    sushi::control::TimeSignature ts;\n    ts.numerator = request->numerator();\n    ts.denominator = request->denominator();\n    auto status = _controller->set_time_signature(ts);\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status TimingControlService::GetTimingsEnabled(grpc::ServerContext* /*context*/,\n                                                     const sushi_rpc::GenericVoidValue* /*request*/,\n                                                     sushi_rpc::GenericBoolValue* response)\n{\n    response->set_value(_controller->get_timing_statistics_enabled());\n    return grpc::Status::OK;\n}\n\ngrpc::Status TimingControlService::SetTimingsEnabled(grpc::ServerContext* /*context*/,\n                                                     const sushi_rpc::GenericBoolValue* request,\n                                                     sushi_rpc::CommandResponse* response)\n{\n    _controller->set_timing_statistics_enabled(request->value());\n    response->mutable_status()->set_status(CommandStatus::SUCCESS);\n    return grpc::Status::OK;\n}\n\ngrpc::Status TimingControlService::GetEngineTimings(grpc::ServerContext* /*context*/,\n                                                    const sushi_rpc::GenericVoidValue* /*request*/,\n                                                    sushi_rpc::CpuTimings* response)\n{\n    auto [status, timings] = _controller->get_engine_timings();\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        to_grpc(*response, timings);\n    }\n    return grpc::Status::OK;\n}\n\ngrpc::Status TimingControlService::GetTrackTimings(grpc::ServerContext* /*context*/,\n                                                   const sushi_rpc::TrackIdentifier* request,\n                                                   sushi_rpc::TimingResponse* response)\n{\n    auto [status, timings] = _controller->get_track_timings(request->id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        to_grpc(*response->mutable_timings(), timings);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status TimingControlService::GetProcessorTimings(grpc::ServerContext* /*context*/,\n                                                       const sushi_rpc::ProcessorIdentifier* request,\n                                                       sushi_rpc::TimingResponse* response)\n{\n    auto [status, timings] = _controller->get_processor_timings(request->id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        to_grpc(*response->mutable_timings(), timings);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status TimingControlService::ResetAllTimings(grpc::ServerContext* /*context*/,\n                                                   const sushi_rpc::GenericVoidValue* /*request*/,\n                                                   sushi_rpc::CommandResponse* response)\n{\n    _controller->reset_all_timings();\n    response->mutable_status()->set_status(CommandStatus::SUCCESS);\n    return grpc::Status::OK;\n}\n\ngrpc::Status TimingControlService::ResetTrackTimings(grpc::ServerContext* /*context*/,\n                                                     const sushi_rpc::TrackIdentifier* request,\n                                                     sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->reset_track_timings(request->id());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status TimingControlService::ResetProcessorTimings(grpc::ServerContext* /*context*/,\n                                                         const sushi_rpc::ProcessorIdentifier* request,\n                                                         sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->reset_processor_timings(request->id());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status KeyboardControlService::SendNoteOn(grpc::ServerContext* /*context*/,\n                                                const sushi_rpc::NoteOnRequest*request,\n                                                sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->send_note_on(request->track().id(), request->channel(), request->note(), request->velocity());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status KeyboardControlService::SendNoteOff(grpc::ServerContext* /*context*/,\n                                                 const sushi_rpc::NoteOffRequest* request,\n                                                 sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->send_note_off(request->track().id(), request->channel(), request->note(), request->velocity());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status KeyboardControlService::SendNoteAftertouch(grpc::ServerContext* /*context*/,\n                                                        const sushi_rpc::NoteAftertouchRequest* request,\n                                                        sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->send_note_aftertouch(request->track().id(), request->channel(), request->note(), request->value());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status KeyboardControlService::SendAftertouch(grpc::ServerContext* /*context*/,\n                                                    const sushi_rpc::NoteModulationRequest* request,\n                                                    sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->send_aftertouch(request->track().id(), request->channel(), request->value());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status KeyboardControlService::SendPitchBend(grpc::ServerContext* /*context*/,\n                                                   const sushi_rpc::NoteModulationRequest* request,\n                                                   sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->send_pitch_bend(request->track().id(), request->channel(), request->value());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status KeyboardControlService::SendModulation(grpc::ServerContext* /*context*/,\n                                                    const sushi_rpc::NoteModulationRequest* request,\n                                                    sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->send_modulation(request->track().id(), request->channel(), request->value());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::GetAllProcessors(grpc::ServerContext* /*context*/,\n                                                        const sushi_rpc::GenericVoidValue* /*request*/,\n                                                        sushi_rpc::ProcessorInfoList* response)\n{\n    auto processors = _controller->get_all_processors();\n    for (const auto& processor : processors)\n    {\n        auto info = response->add_processors();\n        to_grpc(*info, processor);\n    }\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::GetAllTracks(grpc::ServerContext* /*context*/,\n                                                    const sushi_rpc::GenericVoidValue* /*request*/,\n                                                    sushi_rpc::TrackInfoList* response)\n{\n    auto tracks = _controller->get_all_tracks();\n    for (const auto& track : tracks)\n    {\n        auto info = response->add_tracks();\n        to_grpc(*info, track);\n    }\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::GetTrackId(grpc::ServerContext* /*context*/,\n                                                  const sushi_rpc::GenericStringValue* request,\n                                                  sushi_rpc::TrackIdentifierResponse* response)\n{\n    auto [status, id] = _controller->get_track_id(request->value());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        response->set_id(id);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::GetTrackInfo(grpc::ServerContext* /*context*/,\n                                                    const sushi_rpc::TrackIdentifier* request,\n                                                    sushi_rpc::TrackInfoResponse* response)\n{\n    auto [status, track] = _controller->get_track_info(request->id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        to_grpc(*response->mutable_info(), track);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::GetTrackProcessors(grpc::ServerContext* /*context*/,\n                                                          const sushi_rpc::TrackIdentifier* request,\n                                                          sushi_rpc::ProcessorInfoListResponse* response)\n{\n    auto [status, processors] = _controller->get_track_processors(request->id());\n    for (const auto& processor : processors)\n    {\n        auto info = response->add_processors();\n        to_grpc(*info, processor);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::GetProcessorId(grpc::ServerContext* /*context*/,\n                                                      const sushi_rpc::GenericStringValue* request,\n                                                      sushi_rpc::ProcessorIdentifierResponse* response)\n{\n    auto [status, id] = _controller->get_processor_id(request->value());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        response->set_id(id);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::GetProcessorInfo(grpc::ServerContext* /*context*/,\n                                                        const sushi_rpc::ProcessorIdentifier* request,\n                                                        sushi_rpc::ProcessorInfoResponse* response)\n{\n    auto [status, processor] = _controller->get_processor_info(request->id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        to_grpc(*response->mutable_processor(), processor);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::GetProcessorBypassState(grpc::ServerContext* /*context*/,\n                                                               const sushi_rpc::ProcessorIdentifier* request,\n                                                               sushi_rpc::BoolResponse* response)\n{\n    auto [status, state] = _controller->get_processor_bypass_state(request->id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        response->set_value(state);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::GetProcessorState(grpc::ServerContext* /*context*/,\n                                                         const sushi_rpc::ProcessorIdentifier* request,\n                                                         sushi_rpc::ProcessorStateResponse* response)\n{\n    auto [status, state] = _controller->get_processor_state(request->id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        to_grpc(*response->mutable_state(), state);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::SetProcessorBypassState(grpc::ServerContext* /*context*/,\n                                                               const sushi_rpc::ProcessorBypassStateSetRequest* request,\n                                                               sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->set_processor_bypass_state(request->processor().id(), request->value());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::SetProcessorState(grpc::ServerContext* /*context*/,\n                                                         const sushi_rpc::ProcessorStateSetRequest* request,\n                                                         sushi_rpc::CommandResponse* response)\n{\n    sushi::control::ProcessorState sushi_state;\n    to_sushi_ext(sushi_state, request->state());\n    auto status = _controller->set_processor_state(request->processor().id(), sushi_state);\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::CreateTrack(grpc::ServerContext* /*context*/,\n                                                   const sushi_rpc::CreateTrackRequest* request,\n                                                   sushi_rpc::CommandResponse* response)\n{\n    auto thread = (request->thread().has_value() ? std::optional<int>(request->thread().value()) : std::nullopt);\n    auto status = _controller->create_track(request->name(), request->channels(), thread);\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::CreateMultibusTrack(grpc::ServerContext* /*context*/,\n                                                           const sushi_rpc::CreateMultibusTrackRequest* request,\n                                                           sushi_rpc::CommandResponse* response)\n{\n    auto thread = (request->thread().has_value() ? std::optional<int>(request->thread().value()) : std::nullopt);\n    auto status = _controller->create_multibus_track(request->name(), request->buses(), thread);\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::CreatePreTrack(grpc::ServerContext* /*context*/,\n                                                      const sushi_rpc::CreatePreTrackRequest* request,\n                                                      sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->create_pre_track(request->name());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::CreatePostTrack(grpc::ServerContext* /*context*/,\n                                                       const sushi_rpc::CreatePostTrackRequest* request,\n                                                       sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->create_post_track(request->name());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::DeleteTrack(grpc::ServerContext* /*context*/,\n                                                   const sushi_rpc::TrackIdentifier* request,\n                                                   sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->delete_track(request->id());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::CreateProcessorOnTrack(grpc::ServerContext* /*context*/,\n                                                              const sushi_rpc::CreateProcessorRequest* request,\n                                                              sushi_rpc::CommandResponse* response)\n{\n    std::optional<int> before_processor = std::nullopt;\n    if (request->position().add_to_back() == false)\n    {\n        before_processor = request->position().before_processor().id();\n    }\n    auto status = _controller->create_processor_on_track(request->name(),\n                                                         request->uid(),\n                                                         request->path(),\n                                                         to_sushi_ext(request->type().type()),\n                                                         request->track().id(),\n                                                         before_processor);\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::MoveProcessorOnTrack(grpc::ServerContext* /*context*/,\n                                                            const sushi_rpc::MoveProcessorRequest*request,\n                                                            sushi_rpc::CommandResponse* response)\n{\n    std::optional<int> before_processor = std::nullopt;\n    if (request->position().add_to_back() == false)\n    {\n        before_processor = request->position().before_processor().id();\n    }\n    auto status = _controller->move_processor_on_track(request->processor().id(),\n                                                       request->source_track().id(),\n                                                       request->dest_track().id(),\n                                                       before_processor);\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioGraphControlService::DeleteProcessorFromTrack(grpc::ServerContext* /*context*/,\n                                                                const sushi_rpc::DeleteProcessorRequest* request,\n                                                                sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->delete_processor_from_track(request->processor().id(),\n                                                           request->track().id());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::GetTrackParameters(grpc::ServerContext* /*context*/,\n                                                         const sushi_rpc::TrackIdentifier* request,\n                                                         sushi_rpc::ParameterInfoListResponse* response)\n{\n    auto [status, parameters] = _controller->get_track_parameters(request->id());\n    for (const auto& parameter : parameters)\n    {\n        auto info = response->add_parameters();\n        to_grpc(*info, parameter);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::GetParameterId(grpc::ServerContext* /*context*/,\n                                                     const sushi_rpc::ParameterIdRequest* request,\n                                                     sushi_rpc::ParameterIdentifierResponse* response)\n{\n    auto [status, id] = _controller->get_parameter_id(request->processor().id(), request->parametername());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        response->mutable_id()->set_parameter_id(id);\n        response->mutable_id()->set_processor_id(request->processor().id());\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::GetParameterInfo(grpc::ServerContext* /*context*/,\n                                                       const sushi_rpc::ParameterIdentifier* request,\n                                                       sushi_rpc::ParameterInfoResponse* response)\n{\n    auto [status, parameter] = _controller->get_parameter_info(request->processor_id(), request->parameter_id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        to_grpc(*response->mutable_info(), parameter);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::GetParameterValue(grpc::ServerContext* /*context*/,\n                                                        const sushi_rpc::ParameterIdentifier* request,\n                                                        sushi_rpc::FloatResponse* response)\n{\n    auto [status, value] = _controller->get_parameter_value(request->processor_id(), request->parameter_id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        response->set_value(value);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::GetParameterValueInDomain(grpc::ServerContext* /*context*/,\n                                                                const sushi_rpc::ParameterIdentifier* request,\n                                                                sushi_rpc::FloatResponse* response)\n{\n    auto [status, value] = _controller->get_parameter_value_in_domain(request->processor_id(), request->parameter_id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        response->set_value(value);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::GetParameterValueAsString(grpc::ServerContext* /*context*/,\n                                                            const sushi_rpc::ParameterIdentifier* request,\n                                                            sushi_rpc::StringResponse* response)\n{\n    auto [status, value] = _controller->get_parameter_value_as_string(request->processor_id(), request->parameter_id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        response->set_value(value);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::SetParameterValue(grpc::ServerContext* /*context*/,\n                                                        const sushi_rpc::ParameterValue* request,\n                                                        sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->set_parameter_value(request->parameter().processor_id(),\n                                                   request->parameter().parameter_id(),\n                                                   request->value());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status ProgramControlService::GetProcessorCurrentProgram(grpc::ServerContext* /*context*/,\n                                                               const sushi_rpc::ProcessorIdentifier* request,\n                                                               sushi_rpc::ProgramIdentifierResponse* response)\n{\n    auto [status, program] = _controller->get_processor_current_program(request->id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        response->set_program(program);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ProgramControlService::GetProcessorCurrentProgramName(grpc::ServerContext* /*context*/,\n                                                                   const sushi_rpc::ProcessorIdentifier* request,\n                                                                   sushi_rpc::StringResponse* response)\n{\n    auto [status, program] = _controller->get_processor_current_program_name(request->id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        response->set_value(program);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ProgramControlService::GetProcessorProgramName(grpc::ServerContext* /*context*/,\n                                                            const sushi_rpc::ProcessorProgramIdentifier* request,\n                                                            sushi_rpc::StringResponse* response)\n{\n    auto [status, program] = _controller->get_processor_program_name(request->processor().id(), request->program());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        response->set_value(program);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ProgramControlService::GetProcessorPrograms(grpc::ServerContext* /*context*/,\n                                                         const sushi_rpc::ProcessorIdentifier* request,\n                                                         sushi_rpc::ProgramInfoListResponse* response)\n{\n    auto [status, programs] = _controller->get_processor_programs(request->id());\n    int id = 0;\n    for (auto& program : programs)\n    {\n        auto info = response->add_programs();\n        info->set_name(program);\n        info->mutable_id()->set_program(id++);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ProgramControlService::SetProcessorProgram(grpc::ServerContext* /*context*/,\n                                                        const sushi_rpc::ProcessorProgramSetRequest* request,\n                                                        sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->set_processor_program(request->processor().id(), request->program().program());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::GetProcessorParameters(grpc::ServerContext* /*context*/,\n                                                             const sushi_rpc::ProcessorIdentifier* request,\n                                                             sushi_rpc::ParameterInfoListResponse* response)\n{\n    auto [status, parameters] = _controller->get_processor_parameters(request->id());\n    for (const auto& parameter : parameters)\n    {\n        auto info = response->add_parameters();\n        to_grpc(*info, parameter);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::GetTrackProperties(grpc::ServerContext* /*context*/,\n                                                         const sushi_rpc::TrackIdentifier* request,\n                                                         sushi_rpc::PropertyInfoListResponse* response)\n{\n    auto [status, properties] = _controller->get_track_properties(request->id());\n    for (const auto& property : properties)\n    {\n        auto info = response->add_properties();\n        to_grpc(*info, property);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::GetProcessorProperties(grpc::ServerContext* /*context*/,\n                                                             const sushi_rpc::ProcessorIdentifier* request,\n                                                             sushi_rpc::PropertyInfoListResponse* response)\n{\n    auto [status, properties] = _controller->get_processor_properties(request->id());\n    for (const auto& property : properties)\n    {\n        auto info = response->add_properties();\n        to_grpc(*info, property);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::GetPropertyId(grpc::ServerContext* /*context*/,\n                                                    const sushi_rpc::PropertyIdRequest* request,\n                                                    sushi_rpc::PropertyIdentifierResponse* response)\n{\n    auto [status, id] = _controller->get_property_id(request->processor().id(), request->property_name());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        response->mutable_id()->set_property_id(id);\n        response->mutable_id()->set_processor_id(request->processor().id());\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::GetPropertyInfo(grpc::ServerContext* /*context*/,\n                                                      const sushi_rpc::PropertyIdentifier* request,\n                                                      sushi_rpc::PropertyInfoResponse* response)\n{\n    auto [status, property] = _controller->get_property_info(request->processor_id(), request->property_id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        to_grpc(*response->mutable_info(), property);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::GetPropertyValue(grpc::ServerContext* /*context*/,\n                                                       const sushi_rpc::PropertyIdentifier* request,\n                                                       sushi_rpc::StringResponse* response)\n{\n    auto [status, value] = _controller->get_property_value(request->processor_id(), request->property_id());\n    if (status == sushi::control::ControlStatus::OK)\n    {\n        response->set_value(std::move(value));\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status ParameterControlService::SetPropertyValue(grpc::ServerContext* /*context*/,\n                                                       const sushi_rpc::PropertyValue* request,\n                                                       sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->set_property_value(request->property().processor_id(),\n                                                  request->property().property_id(),\n                                                  request->value());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::GetInputPorts(grpc::ServerContext* /*context*/,\n                                               const sushi_rpc::GenericVoidValue* /*request*/,\n                                               sushi_rpc::GenericIntValue* response)\n{\n    response->set_value(_midi_controller->get_input_ports());\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::GetOutputPorts(grpc::ServerContext* /*context*/,\n                                                const sushi_rpc::GenericVoidValue* /*request*/,\n                                                sushi_rpc::GenericIntValue* response)\n{\n    response->set_value(_midi_controller->get_output_ports());\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::GetAllKbdInputConnections(grpc::ServerContext* /*context*/,\n                                                           const sushi_rpc::GenericVoidValue* /*request*/,\n                                                           sushi_rpc::MidiKbdConnectionList* response)\n{\n    auto input_connections = _midi_controller->get_all_kbd_input_connections();\n    for (const auto& connection : input_connections)\n    {\n        auto info = response->add_connections();\n        to_grpc(*info, connection);\n    }\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::GetAllKbdOutputConnections(grpc::ServerContext* /*context*/,\n                                                            const sushi_rpc::GenericVoidValue* /*request*/,\n                                                            sushi_rpc::MidiKbdConnectionList* response)\n{\n    auto output_connections = _midi_controller->get_all_kbd_output_connections();\n    for (const auto& connection : output_connections)\n    {\n        auto info = response->add_connections();\n        to_grpc(*info, connection);\n    }\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::GetAllCCInputConnections(grpc::ServerContext* /*context*/,\n                                                          const sushi_rpc::GenericVoidValue* /*request*/,\n                                                          sushi_rpc::MidiCCConnectionList* response)\n{\n    auto output_connections = _midi_controller->get_all_cc_input_connections();\n    for (const auto& connection : output_connections)\n    {\n        auto info = response->add_connections();\n        to_grpc(*info, connection);\n    }\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::GetAllPCInputConnections(grpc::ServerContext* /*context*/,\n                                                          const sushi_rpc::GenericVoidValue* /*request*/,\n                                                          sushi_rpc::MidiPCConnectionList* response)\n{\n    auto input_connections = _midi_controller->get_all_pc_input_connections();\n    for (const auto& connection : input_connections)\n    {\n        auto info = response->add_connections();\n        to_grpc(*info, connection);\n    }\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::GetCCInputConnectionsForProcessor(grpc::ServerContext* /*context*/,\n                                                                   const sushi_rpc::ProcessorIdentifier* request,\n                                                                   sushi_rpc::MidiCCConnectionListResponse* response)\n{\n    const auto processor_id = request->id();\n    auto output_connections = _midi_controller->get_cc_input_connections_for_processor(processor_id);\n    if(output_connections.first == sushi::control::ControlStatus::OK)\n    {\n        for (const auto& connection : output_connections.second)\n        {\n            auto info = response->add_connections();\n            to_grpc(*info, connection);\n        }\n    }\n    response->mutable_status()->set_status(to_grpc(output_connections.first));\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::GetPCInputConnectionsForProcessor(grpc::ServerContext* /*context*/,\n                                                                   const sushi_rpc::ProcessorIdentifier* request,\n                                                                   sushi_rpc::MidiPCConnectionListResponse* response)\n{\n    const auto processor_id = request->id();\n    auto input_connections = _midi_controller->get_pc_input_connections_for_processor(processor_id);\n    if(input_connections.first == sushi::control::ControlStatus::OK)\n    {\n        for (const auto& connection : input_connections.second)\n        {\n            auto info = response->add_connections();\n            to_grpc(*info, connection);\n        }\n    }\n    response->mutable_status()->set_status(to_grpc(input_connections.first));\n    return grpc::Status::OK;\n}\n\n\ngrpc::Status MidiControlService::GetMidiClockOutputEnabled(grpc::ServerContext* /*context*/,\n                                                           const sushi_rpc::GenericIntValue* request,\n                                                           sushi_rpc::GenericBoolValue* response)\n{\n    response->set_value(_midi_controller->get_midi_clock_output_enabled(request->value()));\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::SetMidiClockOutputEnabled(grpc::ServerContext* /*context*/,\n                                                           const sushi_rpc::MidiClockSetRequest* request,\n                                                           sushi_rpc::CommandResponse* response)\n{\n    auto status = _midi_controller->set_midi_clock_output_enabled(request->enabled(), request->port());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::ConnectKbdInputToTrack(grpc::ServerContext* /*context*/,\n                                                        const sushi_rpc::MidiKbdConnection* request,\n                                                        sushi_rpc::CommandResponse* response)\n{\n    const auto track_id = request->track();\n    const auto channel = request->channel().channel();\n    const auto port = request->port();\n    const auto raw_midi = request->raw_midi();\n    const auto midi_channel = to_sushi_ext(channel);\n\n    const auto status = _midi_controller->connect_kbd_input_to_track(track_id.id(), midi_channel, port, raw_midi);\n\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::ConnectKbdOutputFromTrack(grpc::ServerContext* /*context*/,\n                                                           const sushi_rpc::MidiKbdConnection* request,\n                                                           sushi_rpc::CommandResponse* response)\n{\n    const auto track_id = request->track();\n    const auto channel = request->channel().channel();\n    const auto port = request->port();\n    const auto midi_channel = to_sushi_ext(channel);\n\n    const auto status = _midi_controller->connect_kbd_output_from_track(track_id.id(), midi_channel, port);\n\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::ConnectCCToParameter(grpc::ServerContext* /*context*/,\n                                                      const sushi_rpc::MidiCCConnection* request,\n                                                      sushi_rpc::CommandResponse* response)\n{\n    const auto midi_channel = to_sushi_ext(request->channel().channel());\n    const auto status = _midi_controller->connect_cc_to_parameter(request->parameter().processor_id(),\n                                                                  request->parameter().parameter_id(),\n                                                                  midi_channel,\n                                                                  request->port(),\n                                                                  request->cc_number(),\n                                                                  request->min_range(),\n                                                                  request->max_range(),\n                                                                  request->relative_mode());\n\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::ConnectPCToProcessor(grpc::ServerContext* /*context*/,\n                                                      const sushi_rpc::MidiPCConnection* request,\n                                                      sushi_rpc::CommandResponse* response)\n{\n    const auto processor_id = request->processor().id();\n    const MidiChannel_Channel channel = request->channel().channel();\n    const auto port = request->port();\n\n    sushi::control::MidiChannel midi_channel = to_sushi_ext(channel);\n\n    const auto status = _midi_controller->connect_pc_to_processor(processor_id, midi_channel, port);\n\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::DisconnectKbdInput(grpc::ServerContext* /*context*/,\n                                                    const sushi_rpc::MidiKbdConnection* request,\n                                                    sushi_rpc::CommandResponse* response)\n{\n    const auto track_id = request->track();\n    const auto channel = request->channel().channel();\n    const auto port = request->port();\n    const auto midi_channel = to_sushi_ext(channel);\n    const auto raw_midi = request->raw_midi();\n    const auto status = _midi_controller->disconnect_kbd_input(track_id.id(), midi_channel, port, raw_midi);\n\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::DisconnectKbdOutput(grpc::ServerContext* /*context*/,\n                                                     const sushi_rpc::MidiKbdConnection* request,\n                                                     sushi_rpc::CommandResponse* response)\n{\n    const auto track_id = request->track();\n    const auto channel = request->channel().channel();\n    const auto port = request->port();\n    const auto midi_channel = to_sushi_ext(channel);\n\n    const auto status = _midi_controller->disconnect_kbd_output(track_id.id(), midi_channel, port);\n\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::DisconnectCC(grpc::ServerContext* /*context*/,\n                                              const sushi_rpc::MidiCCConnection* request,\n                                              sushi_rpc::CommandResponse* response)\n{\n    const auto midi_channel = to_sushi_ext(request->channel().channel());\n    const auto status = _midi_controller->disconnect_cc(request->parameter().processor_id(),\n                                                        midi_channel,\n                                                        request->port(),\n                                                        request->cc_number());\n\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::DisconnectPC(grpc::ServerContext* /*context*/,\n                                              const sushi_rpc::MidiPCConnection* request,\n                                              sushi_rpc::CommandResponse* response)\n{\n    const auto processor_id = request->processor().id();\n    const MidiChannel_Channel channel = request->channel().channel();\n    const auto port = request->port();\n    sushi::control::MidiChannel midi_channel = to_sushi_ext(channel);\n\n    const auto status = _midi_controller->disconnect_pc(processor_id, midi_channel, port);\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::DisconnectAllCCFromProcessor(grpc::ServerContext* /*context*/,\n                                                              const sushi_rpc::ProcessorIdentifier* request,\n                                                              sushi_rpc::CommandResponse* response)\n{\n    const auto processor_id = request->id();\n    const auto status = _midi_controller->disconnect_all_cc_from_processor(processor_id);\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status MidiControlService::DisconnectAllPCFromProcessor(grpc::ServerContext* /*context*/,\n                                                              const sushi_rpc::ProcessorIdentifier* request,\n                                                              sushi_rpc::CommandResponse* response)\n{\n    const auto processor_id = request->id();\n    const auto status = _midi_controller->disconnect_all_pc_from_processor(processor_id);\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioRoutingControlService::GetAllInputConnections(grpc::ServerContext* /*context*/,\n                                                                const sushi_rpc::GenericVoidValue* /*request*/,\n                                                                sushi_rpc::AudioConnectionList* response)\n{\n    auto connections = _controller->get_all_input_connections();\n    for (const auto& connection : connections)\n    {\n        auto c = response->add_connections();\n        to_grpc(*c, connection);\n    }\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioRoutingControlService::GetAllOutputConnections(grpc::ServerContext* /*context*/,\n                                                                 const sushi_rpc::GenericVoidValue* /*request*/,\n                                                                 sushi_rpc::AudioConnectionList* response)\n{\n    auto connections = _controller->get_all_output_connections();\n    for (const auto& connection : connections)\n    {\n        auto c = response->add_connections();\n        to_grpc(*c, connection);\n    }\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioRoutingControlService::GetInputConnectionsForTrack(grpc::ServerContext* /*context*/,\n                                                                     const sushi_rpc::TrackIdentifier* request,\n                                                                     sushi_rpc::AudioConnectionListResponse* response)\n{\n    auto [status, connections] = _controller->get_input_connections_for_track(request->id());\n    for (const auto& connection : connections)\n    {\n        auto c = response->add_connections();\n        to_grpc(*c, connection);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioRoutingControlService::GetOutputConnectionsForTrack(grpc::ServerContext* /*context*/,\n                                                                      const sushi_rpc::TrackIdentifier* request,\n                                                                      sushi_rpc::AudioConnectionListResponse* response)\n{\n    auto [status, connections] = _controller->get_output_connections_for_track(request->id());\n    for (const auto& connection : connections)\n    {\n        auto c = response->add_connections();\n        to_grpc(*c, connection);\n    }\n    response->mutable_status()->set_status(to_grpc(status));\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioRoutingControlService::ConnectInputChannelToTrack(grpc::ServerContext* /*context*/,\n                                                                    const sushi_rpc::AudioConnection* request,\n                                                                    sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->connect_input_channel_to_track(request->track().id(),\n                                                              request->track_channel(),\n                                                              request->engine_channel());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioRoutingControlService::ConnectOutputChannelFromTrack(grpc::ServerContext* /*context*/,\n                                                                       const sushi_rpc::AudioConnection* request,\n                                                                       sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->connect_output_channel_to_track(request->track().id(),\n                                                               request->track_channel(),\n                                                               request->engine_channel());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioRoutingControlService::DisconnectInput(grpc::ServerContext* /*context*/,\n                                                         const sushi_rpc::AudioConnection* request,\n                                                         sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->disconnect_input(request->track().id(),\n                                                request->track_channel(),\n                                                request->engine_channel());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioRoutingControlService::DisconnectOutput(grpc::ServerContext* /*context*/,\n                                                          const sushi_rpc::AudioConnection* request,\n                                                          sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->disconnect_output(request->track().id(),\n                                                 request->track_channel(),\n                                                 request->engine_channel());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status AudioRoutingControlService::DisconnectAllInputsFromTrack(grpc::ServerContext* /*context*/,\n                                                                      const sushi_rpc::TrackIdentifier* request,\n                                                                      sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->disconnect_all_inputs_from_track(request->id());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\n// This function is deprecated and should be removed eventually.\n/*grpc::Status AudioRoutingControlService::DisconnectAllOutputFromTrack(grpc::ServerContext* context,\n                                                                      const sushi_rpc::TrackIdentifier* request,\n                                                                      sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->disconnect_all_outputs_from_track(request->id());\n    return to_grpc_status(status);\n}*/\n\ngrpc::Status AudioRoutingControlService::DisconnectAllOutputsFromTrack(grpc::ServerContext* /*context*/,\n                                                                       const sushi_rpc::TrackIdentifier* request,\n                                                                       sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->disconnect_all_outputs_from_track(request->id());\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status CvGateControlService::GetCvInputChannelCount(grpc::ServerContext* context,\n                                                          const sushi_rpc::GenericVoidValue* request,\n                                                          sushi_rpc::GenericIntValue* response)\n{\n    return Service::GetCvInputChannelCount(context, request, response);\n}\n\ngrpc::Status CvGateControlService::GetCvOutputChannelCount(grpc::ServerContext* context,\n                                                           const sushi_rpc::GenericVoidValue* request,\n                                                           sushi_rpc::GenericIntValue* response)\n{\n    return Service::GetCvOutputChannelCount(context, request, response);\n}\n\ngrpc::Status CvGateControlService::GetAllCvInputConnections(grpc::ServerContext* context,\n                                                            const sushi_rpc::GenericVoidValue* request,\n                                                            sushi_rpc::CvConnectionList* response)\n{\n    return Service::GetAllCvInputConnections(context, request, response);\n}\n\ngrpc::Status CvGateControlService::GetAllCvOutputConnections(grpc::ServerContext* context,\n                                                             const sushi_rpc::GenericVoidValue* request,\n                                                             sushi_rpc::CvConnectionList* response)\n{\n    return Service::GetAllCvOutputConnections(context, request, response);\n}\n\ngrpc::Status CvGateControlService::GetAllGateInputConnections(grpc::ServerContext* context,\n                                                              const sushi_rpc::GenericVoidValue* request,\n                                                              sushi_rpc::GateConnectionList* response)\n{\n    return Service::GetAllGateInputConnections(context, request, response);\n}\n\ngrpc::Status CvGateControlService::GetAllGateOutputConnections(grpc::ServerContext* context,\n                                                               const sushi_rpc::GenericVoidValue* request,\n                                                               sushi_rpc::GateConnectionList* response)\n{\n    return Service::GetAllGateOutputConnections(context, request, response);\n}\n\ngrpc::Status CvGateControlService::GetCvInputConnectionsForProcessor(grpc::ServerContext* context,\n                                                                     const sushi_rpc::ProcessorIdentifier* request,\n                                                                     sushi_rpc::CvConnectionListResponse* response)\n{\n    return Service::GetCvInputConnectionsForProcessor(context, request, response);\n}\n\ngrpc::Status CvGateControlService::GetCvOutputConnectionsForProcessor(grpc::ServerContext* context,\n                                                                      const sushi_rpc::ProcessorIdentifier* request,\n                                                                      sushi_rpc::CvConnectionListResponse* response)\n{\n    return Service::GetCvOutputConnectionsForProcessor(context, request, response);\n}\n\ngrpc::Status CvGateControlService::GetGateInputConnectionsForProcessor(grpc::ServerContext* context,\n                                                                       const sushi_rpc::ProcessorIdentifier* request,\n                                                                       sushi_rpc::GateConnectionListResponse* response)\n{\n    return Service::GetGateInputConnectionsForProcessor(context, request, response);\n}\n\ngrpc::Status CvGateControlService::GetGateOutputConnectionsForProcessor(grpc::ServerContext* context,\n                                                                        const sushi_rpc::ProcessorIdentifier* request,\n                                                                        sushi_rpc::GateConnectionListResponse* response)\n{\n    return Service::GetGateOutputConnectionsForProcessor(context, request, response);\n}\n\ngrpc::Status CvGateControlService::ConnectCvInputToParameter(grpc::ServerContext* context,\n                                                             const sushi_rpc::CvConnection* request,\n                                                             sushi_rpc::CommandResponse* response)\n{\n    return Service::ConnectCvInputToParameter(context, request, response);\n}\n\ngrpc::Status CvGateControlService::ConnectCvOutputFromParameter(grpc::ServerContext* context,\n                                                                const sushi_rpc::CvConnection* request,\n                                                                sushi_rpc::CommandResponse* response)\n{\n    return Service::ConnectCvOutputFromParameter(context, request, response);\n}\n\ngrpc::Status CvGateControlService::ConnectGateInputToProcessor(grpc::ServerContext* context,\n                                                               const sushi_rpc::GateConnection* request,\n                                                               sushi_rpc::CommandResponse* response)\n{\n    return Service::ConnectGateInputToProcessor(context, request, response);\n}\n\ngrpc::Status CvGateControlService::ConnectGateOutputFromProcessor(grpc::ServerContext* context,\n                                                                  const sushi_rpc::GateConnection* request,\n                                                                  sushi_rpc::CommandResponse* response)\n{\n    return Service::ConnectGateOutputFromProcessor(context, request, response);\n}\n\ngrpc::Status CvGateControlService::DisconnectCvInput(grpc::ServerContext* context,\n                                                     const sushi_rpc::CvConnection* request,\n                                                     sushi_rpc::CommandResponse* response)\n{\n    return Service::DisconnectCvInput(context, request, response);\n}\n\ngrpc::Status CvGateControlService::DisconnectCvOutput(grpc::ServerContext* context,\n                                                      const sushi_rpc::CvConnection* request,\n                                                      sushi_rpc::CommandResponse* response)\n{\n    return Service::DisconnectCvOutput(context, request, response);\n}\n\ngrpc::Status CvGateControlService::DisconnectGateInput(grpc::ServerContext* context,\n                                                       const sushi_rpc::GateConnection* request,\n                                                       sushi_rpc::CommandResponse* response)\n{\n    return Service::DisconnectGateInput(context, request, response);\n}\n\ngrpc::Status CvGateControlService::DisconnectGateOutput(grpc::ServerContext* context,\n                                                        const sushi_rpc::GateConnection* request,\n                                                        sushi_rpc::CommandResponse* response)\n{\n    return Service::DisconnectGateOutput(context, request, response);\n}\n\ngrpc::Status CvGateControlService::DisconnectAllCvInputsFromProcessor(grpc::ServerContext* context,\n                                                                      const sushi_rpc::ProcessorIdentifier* request,\n                                                                      sushi_rpc::CommandResponse* response)\n{\n    return Service::DisconnectAllCvInputsFromProcessor(context, request, response);\n}\n\ngrpc::Status CvGateControlService::DisconnectAllCvOutputsFromProcessor(grpc::ServerContext* context,\n                                                                       const sushi_rpc::ProcessorIdentifier* request,\n                                                                       sushi_rpc::CommandResponse* response)\n{\n    return Service::DisconnectAllCvOutputsFromProcessor(context, request, response);\n}\n\ngrpc::Status CvGateControlService::DisconnectAllGateInputsFromProcessor(grpc::ServerContext* context,\n                                                                        const sushi_rpc::ProcessorIdentifier* request,\n                                                                        sushi_rpc::CommandResponse* response)\n{\n    return Service::DisconnectAllGateInputsFromProcessor(context, request, response);\n}\n\ngrpc::Status CvGateControlService::DisconnectAllGateOutputsFromProcessor(grpc::ServerContext* context,\n                                                                         const sushi_rpc::ProcessorIdentifier* request,\n                                                                         sushi_rpc::CommandResponse* response)\n{\n    return Service::DisconnectAllGateOutputsFromProcessor(context, request, response);\n}\n\ngrpc::Status OscControlService::GetSendIP(grpc::ServerContext* /*context*/,\n                                          const sushi_rpc::GenericVoidValue* /*request*/,\n                                          sushi_rpc::GenericStringValue* response)\n{\n  response->set_value(_controller->get_send_ip());\n  return grpc::Status::OK;\n}\n\ngrpc::Status OscControlService::GetSendPort(grpc::ServerContext* /*context*/,\n                                            const sushi_rpc::GenericVoidValue* /*request*/,\n                                            sushi_rpc::GenericIntValue* response)\n{\n    response->set_value(_controller->get_send_port());\n    return grpc::Status::OK;\n}\n\ngrpc::Status OscControlService::GetReceivePort(grpc::ServerContext* /*context*/,\n                                               const sushi_rpc::GenericVoidValue* /*request*/,\n                                               sushi_rpc::GenericIntValue* response)\n{\n    response->set_value(_controller->get_receive_port());\n    return grpc::Status::OK;\n}\n\ngrpc::Status OscControlService::GetEnabledParameterOutputs(grpc::ServerContext* /*context*/,\n                                                           const sushi_rpc::GenericVoidValue* /*request*/,\n                                                           sushi_rpc::OscParameterOutputList* response)\n{\n    auto enabled_outputs = _controller->get_enabled_parameter_outputs();\n\n    for (const auto& path : enabled_outputs)\n    {\n        response->add_path(path);\n    }\n    return grpc::Status::OK;\n}\n\ngrpc::Status OscControlService::EnableOutputForParameter(grpc::ServerContext* /*context*/,\n                                                         const sushi_rpc::ParameterIdentifier* request,\n                                                         sushi_rpc::CommandResponse* response)\n{\n    const auto processor_id = request->processor_id();\n    const auto parameter_id = request->parameter_id();\n\n    auto status = _controller->enable_output_for_parameter(processor_id, parameter_id);\n\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status OscControlService::DisableOutputForParameter(grpc::ServerContext* /*context*/,\n                                                          const sushi_rpc::ParameterIdentifier* request,\n                                                          sushi_rpc::CommandResponse* response)\n{\n    const auto processor_id = request->processor_id();\n    const auto parameter_id = request->parameter_id();\n\n    auto status = _controller->disable_output_for_parameter(processor_id, parameter_id);\n\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status OscControlService::EnableAllOutput(grpc::ServerContext* /*context*/,\n                                                const sushi_rpc::GenericVoidValue* /*request*/,\n                                                sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->enable_all_output();\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status OscControlService::DisableAllOutput(grpc::ServerContext* /*context*/,\n                                                 const sushi_rpc::GenericVoidValue* /*request*/,\n                                                 sushi_rpc::CommandResponse* response)\n{\n    auto status = _controller->disable_all_output();\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\ngrpc::Status SessionControlService::SaveSession(grpc::ServerContext* /*context*/,\n                                                const sushi_rpc::GenericVoidValue* /*request*/,\n                                                sushi_rpc::SessionState* response)\n{\n    auto session_state = _controller->save_session();\n    to_grpc(*response, session_state);\n    return grpc::Status::OK;\n}\n\ngrpc::Status SessionControlService::RestoreSession(grpc::ServerContext* /*context*/,\n                                                   const sushi_rpc::SessionState* request,\n                                                   sushi_rpc::CommandResponse* response)\n{\n    sushi::control::SessionState sushi_state;\n    to_sushi_ext(sushi_state, *request);\n\n    auto status = _controller->restore_session(sushi_state);\n\n    to_grpc(*response, status);\n    return grpc::Status::OK;\n}\n\nNotificationControlService::NotificationControlService(sushi::control::SushiControl* controller) : _controller{controller},\n                                                                                               _audio_graph_controller{controller->audio_graph_controller()}\n{\n    _controller->subscribe_to_notifications(sushi::control::NotificationType::TRANSPORT_UPDATE, this);\n    _controller->subscribe_to_notifications(sushi::control::NotificationType::CPU_TIMING_UPDATE, this);\n    _controller->subscribe_to_notifications(sushi::control::NotificationType::TRACK_UPDATE, this);\n    _controller->subscribe_to_notifications(sushi::control::NotificationType::PROCESSOR_UPDATE, this);\n    _controller->subscribe_to_notifications(sushi::control::NotificationType::PARAMETER_CHANGE, this);\n    _controller->subscribe_to_notifications(sushi::control::NotificationType::PROPERTY_CHANGE, this);\n    _controller->subscribe_to_notifications(sushi::control::NotificationType::ASYNC_COMMAND_COMPLETION, this);\n}\n\nvoid NotificationControlService::notification(const sushi::control::ControlNotification* notification)\n{\n    switch(notification->type())\n    {\n        case sushi::control::NotificationType::TRANSPORT_UPDATE:\n        {\n            _forward_transport_notification_to_subscribers(notification);\n            break;\n        }\n        case sushi::control::NotificationType::CPU_TIMING_UPDATE:\n        {\n            _forward_cpu_timing_notification_to_subscribers(notification);\n            break;\n        }\n        case sushi::control::NotificationType::TRACK_UPDATE:\n        {\n            _forward_track_notification_to_subscribers(notification);\n            break;\n        }\n        case sushi::control::NotificationType::PROCESSOR_UPDATE:\n        {\n            _forward_processor_notification_to_subscribers(notification);\n            break;\n        }\n        case sushi::control::NotificationType::PARAMETER_CHANGE:\n        {\n            _forward_parameter_notification_to_subscribers(notification);\n            break;\n        }\n        case sushi::control::NotificationType::PROPERTY_CHANGE:\n        {\n            _forward_property_notification_to_subscribers(notification);\n            break;\n        }\n        case sushi::control::NotificationType::ASYNC_COMMAND_COMPLETION:\n        {\n            _forward_async_command_notification_to_subscribers(notification);\n            break;\n        }\n        default:\n            break;\n    }\n}\n\nvoid NotificationControlService::_forward_transport_notification_to_subscribers(const sushi::control::ControlNotification* notification)\n{\n    auto typed_notification = static_cast<const sushi::control::TransportNotification*>(notification);\n    auto notification_content = std::make_shared<TransportUpdate>();\n    auto action = typed_notification->action();\n\n    switch(action)\n    {\n        case sushi::control::TransportAction::TEMPO_CHANGED:\n        {\n            float value = std::get<float>(typed_notification->value());\n            notification_content->set_tempo(value);\n            break;\n        }\n        case sushi::control::TransportAction::PLAYING_MODE_CHANGED:\n        {\n            auto grpc_playing_mode = to_grpc(std::get<sushi::control::PlayingMode>(typed_notification->value()));\n            notification_content->mutable_playing_mode()->set_mode(grpc_playing_mode);\n            break;\n        }\n        case sushi::control::TransportAction::SYNC_MODE_CHANGED:\n        {\n            auto grpc_sync_mode = to_grpc(std::get<sushi::control::SyncMode>(typed_notification->value()));\n            notification_content->mutable_sync_mode()->set_mode(grpc_sync_mode);\n            break;\n        }\n        case sushi::control::TransportAction::TIME_SIGNATURE_CHANGED:\n        {\n            auto mutable_time_signature = notification_content->mutable_time_signature();\n            const auto source_time_signature = std::get<sushi::control::TimeSignature>(typed_notification->value());\n            mutable_time_signature->set_denominator(source_time_signature.denominator);\n            mutable_time_signature->set_numerator(source_time_signature.numerator);\n            break;\n        }\n        default:\n        {\n            assert(false);\n            break;\n        }\n    }\n\n    std::scoped_lock lock(_transport_subscriber_lock);\n    for (auto& subscriber : _transport_subscribers)\n    {\n        subscriber->push(notification_content);\n    }\n}\n\nvoid NotificationControlService::_forward_cpu_timing_notification_to_subscribers(const sushi::control::ControlNotification* notification)\n{\n    auto typed_notification = static_cast<const sushi::control::CpuTimingNotification*>(notification);\n    auto notification_content = std::make_shared<CpuTimings>();\n    to_grpc(*notification_content, typed_notification->cpu_timings());\n\n    std::scoped_lock lock(_timing_subscriber_lock);\n    for (auto& subscriber : _timing_subscribers)\n    {\n        subscriber->push(notification_content);\n    }\n}\n\nvoid NotificationControlService::_forward_track_notification_to_subscribers(const sushi::control::ControlNotification* notification)\n{\n    auto typed_notification = static_cast<const sushi::control::TrackNotification*>(notification);\n    auto notification_content = std::make_shared<TrackUpdate>();\n    auto action = typed_notification->action();\n\n    switch(action)\n    {\n        case sushi::control::TrackAction::ADDED:\n        {\n            notification_content->set_action(TrackUpdate_Action_TRACK_ADDED);\n            break;\n        }\n        case sushi::control::TrackAction::DELETED:\n        {\n            notification_content->set_action(TrackUpdate_Action_TRACK_DELETED);\n            break;\n        }\n        default:\n        {\n            assert(false);\n            break;\n        }\n    }\n\n    notification_content->mutable_track()->set_id(typed_notification->track_id());\n\n    std::scoped_lock lock(_track_subscriber_lock);\n    for (auto& subscriber : _track_subscribers)\n    {\n        subscriber->push(notification_content);\n    }\n}\n\nvoid NotificationControlService::_forward_processor_notification_to_subscribers(const sushi::control::ControlNotification* notification)\n{\n    auto typed_notification = static_cast<const sushi::control::ProcessorNotification*>(notification);\n    auto notification_content = std::make_shared<ProcessorUpdate>();\n    auto action = typed_notification->action();\n\n    switch(action)\n    {\n        case sushi::control::ProcessorAction::ADDED:\n        {\n            notification_content->set_action(ProcessorUpdate_Action_PROCESSOR_ADDED);\n            break;\n        }\n        case sushi::control::ProcessorAction::DELETED:\n        {\n            notification_content->set_action(ProcessorUpdate_Action_PROCESSOR_DELETED);\n            break;\n        }\n        default:\n        {\n            assert(false);\n            break;\n        }\n    }\n\n    notification_content->mutable_processor()->set_id(typed_notification->processor_id());\n    notification_content->mutable_parent_track()->set_id(typed_notification->parent_track_id());\n\n    std::scoped_lock lock(_processor_subscriber_lock);\n    for (auto& subscriber : _processor_subscribers)\n    {\n        subscriber->push(notification_content);\n    }\n}\n\nvoid NotificationControlService::_forward_parameter_notification_to_subscribers(const sushi::control::ControlNotification* notification)\n{\n    auto typed_notification = static_cast<const sushi::control::ParameterChangeNotification*>(notification);\n    auto notification_content = std::make_shared<ParameterUpdate>();\n    notification_content->set_normalized_value(typed_notification->value());\n    notification_content->set_domain_value(typed_notification->domain_value());\n    notification_content->set_formatted_value(typed_notification->formatted_value());\n    notification_content->mutable_parameter()->set_parameter_id(typed_notification->parameter_id());\n    notification_content->mutable_parameter()->set_processor_id(typed_notification->processor_id());\n\n    std::scoped_lock lock(_parameter_subscriber_lock);\n    for (auto& subscriber : _parameter_subscribers)\n    {\n        subscriber->push(notification_content);\n    }\n}\n\nvoid NotificationControlService::_forward_property_notification_to_subscribers(const sushi::control::ControlNotification* notification)\n{\n    auto typed_notification = static_cast<const sushi::control::PropertyChangeNotification*>(notification);\n    auto notification_content = std::make_shared<PropertyValue>();\n    notification_content->set_value(typed_notification->value());\n    notification_content->mutable_property()->set_property_id(typed_notification->parameter_id());\n    notification_content->mutable_property()->set_processor_id(typed_notification->processor_id());\n\n    std::scoped_lock lock(_property_subscriber_lock);\n    for (auto& subscriber : _property_subscribers)\n    {\n        subscriber->push(notification_content);\n    }\n}\n\nvoid NotificationControlService::_forward_async_command_notification_to_subscribers(const sushi::control::ControlNotification* notification)\n{\n    auto typed_notification = static_cast<const sushi::control::CommandCompletionNotification*>(notification);\n    auto notification_content = std::make_shared<AsyncCommandResponse>();\n    notification_content->mutable_status()->set_status(to_grpc(typed_notification->status()));\n    notification_content->set_request_id(typed_notification->id());\n\n    std::scoped_lock lock(_command_subscriber_lock);\n    for (auto& subscriber : _command_subscribers)\n    {\n        subscriber->push(notification_content);\n    }\n}\n\nvoid NotificationControlService::subscribe(SubscribeToTransportChangesCallData* subscriber)\n{\n    std::scoped_lock lock(_transport_subscriber_lock);\n    _transport_subscribers.push_back(subscriber);\n}\n\nvoid NotificationControlService::unsubscribe(SubscribeToTransportChangesCallData* subscriber)\n{\n    std::scoped_lock lock(_transport_subscriber_lock);\n    _transport_subscribers.erase(std::remove(_transport_subscribers.begin(),\n                                             _transport_subscribers.end(),\n                                             subscriber));\n}\n\nvoid NotificationControlService::subscribe(SubscribeToCpuTimingUpdatesCallData* subscriber)\n{\n    std::scoped_lock lock(_timing_subscriber_lock);\n    _timing_subscribers.push_back(subscriber);\n}\n\nvoid NotificationControlService::unsubscribe(SubscribeToCpuTimingUpdatesCallData* subscriber)\n{\n    std::scoped_lock lock(_timing_subscriber_lock);\n    _timing_subscribers.erase(std::remove(_timing_subscribers.begin(),\n                                          _timing_subscribers.end(),\n                                          subscriber));\n}\n\nvoid NotificationControlService::subscribe(SubscribeToTrackChangesCallData* subscriber)\n{\n    std::scoped_lock lock(_track_subscriber_lock);\n    _track_subscribers.push_back(subscriber);\n}\n\nvoid NotificationControlService::unsubscribe(SubscribeToTrackChangesCallData* subscriber)\n{\n    std::scoped_lock lock(_track_subscriber_lock);\n    _track_subscribers.erase(std::remove(_track_subscribers.begin(),\n                                         _track_subscribers.end(),\n                                         subscriber));\n}\n\nvoid NotificationControlService::subscribe(SubscribeToProcessorChangesCallData* subscriber)\n{\n    std::scoped_lock lock(_processor_subscriber_lock);\n    _processor_subscribers.push_back(subscriber);\n}\n\nvoid NotificationControlService::unsubscribe(SubscribeToProcessorChangesCallData* subscriber)\n{\n    std::scoped_lock lock(_processor_subscriber_lock);\n    _processor_subscribers.erase(std::remove(_processor_subscribers.begin(),\n                                             _processor_subscribers.end(),\n                                             subscriber));\n}\n\nvoid NotificationControlService::subscribe(SubscribeToParameterUpdatesCallData* subscriber)\n{\n    std::scoped_lock lock(_parameter_subscriber_lock);\n    _parameter_subscribers.push_back(subscriber);\n}\n\nvoid NotificationControlService::unsubscribe(SubscribeToParameterUpdatesCallData* subscriber)\n{\n    std::scoped_lock lock(_parameter_subscriber_lock);\n    _parameter_subscribers.erase(std::remove(_parameter_subscribers.begin(),\n                                             _parameter_subscribers.end(),\n                                             subscriber));\n}\n\nvoid NotificationControlService::subscribe(SubscribeToPropertyUpdatesCallData* subscriber)\n{\n    std::scoped_lock lock(_property_subscriber_lock);\n    _property_subscribers.push_back(subscriber);\n}\n\nvoid NotificationControlService::unsubscribe(SubscribeToPropertyUpdatesCallData* subscriber)\n{\n    std::scoped_lock lock(_property_subscriber_lock);\n    _property_subscribers.erase(std::remove(_property_subscribers.begin(),\n                                            _property_subscribers.end(),\n                                            subscriber));\n}\n\nvoid NotificationControlService::subscribe(SubscribeToAsyncCommandUpdatesCallData* subscriber)\n{\n    std::scoped_lock lock(_command_subscriber_lock);\n    _command_subscribers.push_back(subscriber);\n}\n\nvoid NotificationControlService::unsubscribe(SubscribeToAsyncCommandUpdatesCallData* subscriber)\n{\n    std::scoped_lock lock(_command_subscriber_lock);\n    _command_subscribers.erase(std::remove(_command_subscribers.begin(),\n                                           _command_subscribers.end(),\n                                           subscriber));\n}\n\nvoid NotificationControlService::delete_all_subscribers()\n{\n    /* Unsubscribe and delete CallData subscribers directly, without\n     * waiting for them to be asynchronously deleted when a worker\n     * thread pulls them of the completion queue. */\n    {\n        std::scoped_lock lock(_transport_subscriber_lock);\n        for (auto& subscriber : _transport_subscribers)\n        {\n            delete subscriber;\n        }\n        _transport_subscribers.clear();\n    }\n\n    {\n        std::scoped_lock lock(_timing_subscriber_lock);\n        for (auto& subscriber : _timing_subscribers)\n        {\n            delete subscriber;\n        }\n        _timing_subscribers.clear();\n    }\n\n    {\n        std::scoped_lock lock(_track_subscriber_lock);\n        for (auto& subscriber : _track_subscribers)\n        {\n            delete subscriber;\n        }\n        _track_subscribers.clear();\n    }\n\n    {\n        std::scoped_lock lock(_parameter_subscriber_lock);\n        for (auto& subscriber : _parameter_subscribers)\n        {\n            delete subscriber;\n        }\n        _parameter_subscribers.clear();\n    }\n\n    {\n        std::scoped_lock lock(_property_subscriber_lock);\n        for (auto& subscriber : _property_subscribers)\n        {\n            delete subscriber;\n        }\n        _processor_subscribers.clear();\n    }\n\n    {\n        std::scoped_lock lock(_processor_subscriber_lock);\n        for (auto& subscriber : _processor_subscribers)\n        {\n            delete subscriber;\n        }\n        _processor_subscribers.clear();\n    }\n\n    {\n        std::scoped_lock lock(_command_subscriber_lock);\n        for (auto& subscriber : _command_subscribers)\n        {\n            delete subscriber;\n        }\n        _command_subscribers.clear();\n    }\n}\n\n\n} // sushi_rpc\n"
  },
  {
    "path": "rpc_interface/src/control_service.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Sushi Control Service, gRPC service for external control of Sushi\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SUSHICONTROLSERVICE_H\n#define SUSHI_SUSHICONTROLSERVICE_H\n\n#include <atomic>\n#include <grpcpp/grpcpp.h>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_UNUSED_PARAMETER\nELK_DISABLE_UNREACHABLE_CODE\n#include \"sushi_rpc.grpc.pb.h\"\nELK_POP_WARNING\n\n#include \"sushi/control_interface.h\"\n#include \"sushi/control_notifications.h\"\n\nnamespace sushi_rpc {\n\nclass SubscribeToTransportChangesCallData;\nclass SubscribeToCpuTimingUpdatesCallData;\nclass SubscribeToTrackChangesCallData;\nclass SubscribeToProcessorChangesCallData;\nclass SubscribeToParameterUpdatesCallData;\nclass SubscribeToPropertyUpdatesCallData;\nclass SubscribeToAsyncCommandUpdatesCallData;\n\nclass SystemControlService : public SystemController::Service\n{\npublic:\n    SystemControlService(sushi::control::SushiControl* controller) : _controller{controller->system_controller()} {}\n\n    grpc::Status GetSushiVersion(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericStringValue* response) override;\n    grpc::Status GetSushiApiVersion(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericStringValue* response) override;\n    grpc::Status GetBuildInfo(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::SushiBuildInfo* response) override;\n    grpc::Status GetInputAudioChannelCount(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericIntValue* response) override;\n    grpc::Status GetOutputAudioChannelCount(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericIntValue* response) override;\n\nprivate:\n    sushi::control::SystemController* _controller;\n};\n\nclass TransportControlService : public TransportController::Service\n{\npublic:\n    TransportControlService(sushi::control::SushiControl* controller) : _controller{controller->transport_controller()} {}\n\n    grpc::Status GetSamplerate(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericFloatValue* response) override;\n    grpc::Status GetPlayingMode(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::PlayingMode* response) override;\n    grpc::Status GetSyncMode(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::SyncMode* response) override;\n    grpc::Status GetTimeSignature(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::TimeSignature* response) override;\n    grpc::Status GetTempo(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericFloatValue* response) override;\n    grpc::Status SetTempo(grpc::ServerContext* context, const sushi_rpc::GenericFloatValue* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status SetPlayingMode(grpc::ServerContext* context, const sushi_rpc::PlayingMode* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status SetSyncMode(grpc::ServerContext* context, const sushi_rpc::SyncMode* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status SetTimeSignature(grpc::ServerContext* context, const sushi_rpc::TimeSignature* request, sushi_rpc::CommandResponse* response) override;\n\nprivate:\n    sushi::control::TransportController* _controller;\n};\n\nclass TimingControlService : public TimingController::Service\n{\npublic:\n    TimingControlService(sushi::control::SushiControl* controller) : _controller{controller->timing_controller()} {}\n\n    grpc::Status GetTimingsEnabled(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericBoolValue* response) override;\n    grpc::Status SetTimingsEnabled(grpc::ServerContext* context, const sushi_rpc::GenericBoolValue* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status GetEngineTimings(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::CpuTimings* response) override;\n    grpc::Status GetTrackTimings(grpc::ServerContext* context, const sushi_rpc::TrackIdentifier* request, sushi_rpc::TimingResponse* response) override;\n    grpc::Status GetProcessorTimings(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::TimingResponse* response) override;\n    grpc::Status ResetAllTimings(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status ResetTrackTimings(grpc::ServerContext* context, const sushi_rpc::TrackIdentifier* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status ResetProcessorTimings(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::CommandResponse* response) override;\n\nprivate:\n    sushi::control::TimingController* _controller;\n};\n\nclass KeyboardControlService : public KeyboardController::Service\n{\npublic:\n    KeyboardControlService(sushi::control::SushiControl* controller) : _controller{controller->keyboard_controller()} {}\n\n    grpc::Status SendNoteOn(grpc::ServerContext* context, const sushi_rpc::NoteOnRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status SendNoteOff(grpc::ServerContext* context, const sushi_rpc::NoteOffRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status SendNoteAftertouch(grpc::ServerContext* context, const sushi_rpc::NoteAftertouchRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status SendAftertouch(grpc::ServerContext* context, const sushi_rpc::NoteModulationRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status SendPitchBend(grpc::ServerContext* context, const sushi_rpc::NoteModulationRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status SendModulation(grpc::ServerContext* context, const sushi_rpc::NoteModulationRequest* request, sushi_rpc::CommandResponse* response) override;\n\nprivate:\n    sushi::control::KeyboardController* _controller;\n};\n\nclass AudioGraphControlService : public AudioGraphController::Service\n{\npublic:\n    AudioGraphControlService(sushi::control::SushiControl* controller) : _controller{controller->audio_graph_controller()} {}\n\n    grpc::Status GetAllProcessors(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::ProcessorInfoList* response) override;\n    grpc::Status GetAllTracks(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::TrackInfoList* response) override;\n    grpc::Status GetTrackId(grpc::ServerContext* context, const sushi_rpc::GenericStringValue* request, sushi_rpc::TrackIdentifierResponse* response) override;\n    grpc::Status GetTrackInfo(grpc::ServerContext* context, const sushi_rpc::TrackIdentifier* request, sushi_rpc::TrackInfoResponse* response) override;\n    grpc::Status GetTrackProcessors(grpc::ServerContext* context, const sushi_rpc::TrackIdentifier* request, sushi_rpc::ProcessorInfoListResponse* response) override;\n    grpc::Status GetProcessorId(grpc::ServerContext* context, const sushi_rpc::GenericStringValue* request, sushi_rpc::ProcessorIdentifierResponse* response) override;\n    grpc::Status GetProcessorInfo(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::ProcessorInfoResponse* response) override;\n    grpc::Status GetProcessorBypassState(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::BoolResponse* response) override;\n    grpc::Status GetProcessorState(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::ProcessorStateResponse* response) override;\n    grpc::Status SetProcessorBypassState(grpc::ServerContext* context, const sushi_rpc::ProcessorBypassStateSetRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status SetProcessorState(grpc::ServerContext* context, const sushi_rpc::ProcessorStateSetRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status CreateTrack(grpc::ServerContext* context, const sushi_rpc::CreateTrackRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status CreateMultibusTrack(grpc::ServerContext* context, const sushi_rpc::CreateMultibusTrackRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status CreatePreTrack(grpc::ServerContext* context, const sushi_rpc::CreatePreTrackRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status CreatePostTrack(grpc::ServerContext* context, const sushi_rpc::CreatePostTrackRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status CreateProcessorOnTrack(grpc::ServerContext* context, const sushi_rpc::CreateProcessorRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status MoveProcessorOnTrack(grpc::ServerContext* context, const sushi_rpc::MoveProcessorRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DeleteProcessorFromTrack(grpc::ServerContext* context, const sushi_rpc::DeleteProcessorRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DeleteTrack(grpc::ServerContext* context, const sushi_rpc::TrackIdentifier* request, sushi_rpc::CommandResponse* response) override;\n\nprivate:\n    sushi::control::AudioGraphController* _controller;\n};\n\nclass ProgramControlService : public ProgramController::Service\n{\npublic:\n    ProgramControlService(sushi::control::SushiControl* controller) : _controller{controller->program_controller()} {}\n\n    grpc::Status GetProcessorCurrentProgram(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::ProgramIdentifierResponse* response) override;\n    grpc::Status GetProcessorCurrentProgramName(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::StringResponse* response) override;\n    grpc::Status GetProcessorProgramName(grpc::ServerContext* context, const sushi_rpc::ProcessorProgramIdentifier* request, sushi_rpc::StringResponse* response) override;\n    grpc::Status GetProcessorPrograms(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::ProgramInfoListResponse* response) override;\n    grpc::Status SetProcessorProgram(grpc::ServerContext* context, const sushi_rpc::ProcessorProgramSetRequest* request, sushi_rpc::CommandResponse* response) override;\n\nprivate:\n    sushi::control::ProgramController* _controller;\n};\n\nclass ParameterControlService : public ParameterController::Service\n{\npublic:\n    ParameterControlService(sushi::control::SushiControl* controller) : _controller{controller->parameter_controller()} {}\n\n    grpc::Status GetTrackParameters(grpc::ServerContext* context, const sushi_rpc::TrackIdentifier* request, sushi_rpc::ParameterInfoListResponse* response) override;\n    grpc::Status GetProcessorParameters(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::ParameterInfoListResponse* response) override;\n    grpc::Status GetParameterId(grpc::ServerContext* context, const sushi_rpc::ParameterIdRequest* request, sushi_rpc::ParameterIdentifierResponse* response) override;\n    grpc::Status GetParameterInfo(grpc::ServerContext* context, const sushi_rpc::ParameterIdentifier* request, sushi_rpc::ParameterInfoResponse* response) override;\n    grpc::Status GetParameterValue(grpc::ServerContext* context, const sushi_rpc::ParameterIdentifier* request, sushi_rpc::FloatResponse* response) override;\n    grpc::Status GetParameterValueInDomain(grpc::ServerContext* context, const sushi_rpc::ParameterIdentifier* request, sushi_rpc::FloatResponse* response) override;\n    grpc::Status GetParameterValueAsString(grpc::ServerContext* context, const sushi_rpc::ParameterIdentifier* request, sushi_rpc::StringResponse* response) override;\n    grpc::Status SetParameterValue(grpc::ServerContext* context, const sushi_rpc::ParameterValue* request, sushi_rpc::CommandResponse* response) override;\n\n    grpc::Status GetTrackProperties(grpc::ServerContext* context, const sushi_rpc::TrackIdentifier* request, sushi_rpc::PropertyInfoListResponse* response) override;\n    grpc::Status GetProcessorProperties(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::PropertyInfoListResponse* response) override;\n    grpc::Status GetPropertyId(grpc::ServerContext* context, const sushi_rpc::PropertyIdRequest* request, sushi_rpc::PropertyIdentifierResponse* response) override;\n    grpc::Status GetPropertyInfo(grpc::ServerContext* context, const sushi_rpc::PropertyIdentifier* request, sushi_rpc::PropertyInfoResponse* response) override;\n    grpc::Status GetPropertyValue(grpc::ServerContext* context, const sushi_rpc::PropertyIdentifier* request, sushi_rpc::StringResponse* response) override;\n    grpc::Status SetPropertyValue(grpc::ServerContext* context, const sushi_rpc::PropertyValue* request, sushi_rpc::CommandResponse* response) override;\n\nprivate:\n    sushi::control::ParameterController* _controller;\n};\n\nclass MidiControlService : public MidiController::Service\n{\npublic:\n    MidiControlService(sushi::control::SushiControl* controller) : _midi_controller{controller->midi_controller()} {}\n\n    grpc::Status GetInputPorts(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericIntValue* response) override;\n    grpc::Status GetOutputPorts(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericIntValue* response) override;\n    grpc::Status GetAllKbdInputConnections(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::MidiKbdConnectionList* response) override;\n    grpc::Status GetAllKbdOutputConnections(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::MidiKbdConnectionList* response) override;\n    grpc::Status GetAllCCInputConnections(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::MidiCCConnectionList* response) override;\n    grpc::Status GetAllPCInputConnections(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::MidiPCConnectionList* response) override;\n    grpc::Status GetCCInputConnectionsForProcessor(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::MidiCCConnectionListResponse* response) override;\n    grpc::Status GetPCInputConnectionsForProcessor(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::MidiPCConnectionListResponse* response) override;\n    grpc::Status GetMidiClockOutputEnabled(grpc::ServerContext* context, const sushi_rpc::GenericIntValue* request, sushi_rpc::GenericBoolValue* response) override;\n    grpc::Status SetMidiClockOutputEnabled(grpc::ServerContext* context, const sushi_rpc::MidiClockSetRequest* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status ConnectKbdInputToTrack(grpc::ServerContext* context, const sushi_rpc::MidiKbdConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status ConnectKbdOutputFromTrack(grpc::ServerContext* context, const sushi_rpc::MidiKbdConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status ConnectCCToParameter(grpc::ServerContext* context, const sushi_rpc::MidiCCConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status ConnectPCToProcessor(grpc::ServerContext* context, const sushi_rpc::MidiPCConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectKbdInput(grpc::ServerContext* context, const sushi_rpc::MidiKbdConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectKbdOutput(grpc::ServerContext* context, const sushi_rpc::MidiKbdConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectCC(grpc::ServerContext* context, const sushi_rpc::MidiCCConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectPC(grpc::ServerContext* context, const sushi_rpc::MidiPCConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectAllCCFromProcessor(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectAllPCFromProcessor(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::CommandResponse* response) override;\n\nprivate:\n    sushi::control::MidiController* _midi_controller;\n};\n\nclass AudioRoutingControlService : public AudioRoutingController::Service\n{\npublic:\n    AudioRoutingControlService(sushi::control::SushiControl* controller) : _controller{controller->audio_routing_controller()} {}\n\n    grpc::Status GetAllInputConnections(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::AudioConnectionList* response) override;\n    grpc::Status GetAllOutputConnections(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::AudioConnectionList* response) override;\n    grpc::Status GetInputConnectionsForTrack(grpc::ServerContext* context, const sushi_rpc::TrackIdentifier* request, sushi_rpc::AudioConnectionListResponse* response) override;\n    grpc::Status GetOutputConnectionsForTrack(grpc::ServerContext* context, const sushi_rpc::TrackIdentifier* request, sushi_rpc::AudioConnectionListResponse* response) override;\n    grpc::Status ConnectInputChannelToTrack(grpc::ServerContext* context, const sushi_rpc::AudioConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status ConnectOutputChannelFromTrack(grpc::ServerContext* context, const sushi_rpc::AudioConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectInput(grpc::ServerContext* context, const sushi_rpc::AudioConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectOutput(grpc::ServerContext* context, const sushi_rpc::AudioConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectAllInputsFromTrack(grpc::ServerContext* context, const sushi_rpc::TrackIdentifier* request, sushi_rpc::CommandResponse* response) override;\n// TODO - DEPRECATE    grpc::Status DisconnectAllOutputFromTrack(grpc::ServerContext* context, const sushi_rpc::TrackIdentifier* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectAllOutputsFromTrack(grpc::ServerContext* context, const sushi_rpc::TrackIdentifier* request, sushi_rpc::CommandResponse* response) override;\n\nprivate:\n    sushi::control::AudioRoutingController* _controller;\n};\n\nclass CvGateControlService : public CvGateController::Service\n{\npublic:\n    CvGateControlService(sushi::control::SushiControl* controller) : _controller{controller->cv_gate_controller()} {}\n\n    grpc::Status GetCvInputChannelCount(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericIntValue* response) override;\n    grpc::Status GetCvOutputChannelCount(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericIntValue* response) override;\n    grpc::Status GetAllCvInputConnections(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::CvConnectionList* response) override;\n    grpc::Status GetAllCvOutputConnections(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::CvConnectionList* response) override;\n    grpc::Status GetAllGateInputConnections(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GateConnectionList* response) override;\n    grpc::Status GetAllGateOutputConnections(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GateConnectionList* response) override;\n    grpc::Status GetCvInputConnectionsForProcessor(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::CvConnectionListResponse* response) override;\n    grpc::Status GetCvOutputConnectionsForProcessor(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::CvConnectionListResponse* response) override;\n    grpc::Status GetGateInputConnectionsForProcessor(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::GateConnectionListResponse* response) override;\n    grpc::Status GetGateOutputConnectionsForProcessor(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::GateConnectionListResponse* response) override;\n    grpc::Status ConnectCvInputToParameter(grpc::ServerContext* context, const sushi_rpc::CvConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status ConnectCvOutputFromParameter(grpc::ServerContext* context, const sushi_rpc::CvConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status ConnectGateInputToProcessor(grpc::ServerContext* context, const sushi_rpc::GateConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status ConnectGateOutputFromProcessor(grpc::ServerContext* context, const sushi_rpc::GateConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectCvInput(grpc::ServerContext* context, const sushi_rpc::CvConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectCvOutput(grpc::ServerContext* context, const sushi_rpc::CvConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectGateInput(grpc::ServerContext* context, const sushi_rpc::GateConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectGateOutput(grpc::ServerContext* context, const sushi_rpc::GateConnection* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectAllCvInputsFromProcessor(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectAllCvOutputsFromProcessor(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectAllGateInputsFromProcessor(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisconnectAllGateOutputsFromProcessor(grpc::ServerContext* context, const sushi_rpc::ProcessorIdentifier* request, sushi_rpc::CommandResponse* response) override;\n\nprivate:\n    sushi::control::CvGateController* _controller;\n};\n\nclass OscControlService : public OscController::Service\n{\npublic:\n    OscControlService(sushi::control::SushiControl* controller) : _controller{controller->osc_controller()} {}\n\n    grpc::Status GetSendIP(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericStringValue* response) override;\n    grpc::Status GetSendPort(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericIntValue* response) override;\n    grpc::Status GetReceivePort(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::GenericIntValue* response) override;\n    grpc::Status GetEnabledParameterOutputs(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::OscParameterOutputList* response) override;\n    grpc::Status EnableOutputForParameter(grpc::ServerContext* context, const sushi_rpc::ParameterIdentifier* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisableOutputForParameter(grpc::ServerContext* context, const sushi_rpc::ParameterIdentifier* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status EnableAllOutput(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::CommandResponse* response) override;\n    grpc::Status DisableAllOutput(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::CommandResponse* response) override;\n\nprivate:\n    sushi::control::OscController* _controller;\n};\n\nclass SessionControlService : public SessionController::Service\n{\npublic:\n    SessionControlService(sushi::control::SushiControl* controller) : _controller{controller->session_controller()} {}\n\n    grpc::Status SaveSession(grpc::ServerContext* context, const sushi_rpc::GenericVoidValue* request, sushi_rpc::SessionState* response) override;\n    grpc::Status RestoreSession(grpc::ServerContext* context, const sushi_rpc::SessionState* request, sushi_rpc::CommandResponse* response) override;\n\nprivate:\n    sushi::control::SessionController* _controller;\n};\n\nusing AsyncService = sushi_rpc::NotificationController::WithAsyncMethod_SubscribeToParameterUpdates<\n                     sushi_rpc::NotificationController::WithAsyncMethod_SubscribeToProcessorChanges<\n                     sushi_rpc::NotificationController::WithAsyncMethod_SubscribeToTrackChanges<\n                     sushi_rpc::NotificationController::WithAsyncMethod_SubscribeToEngineCpuTimingUpdates<\n                     sushi_rpc::NotificationController::WithAsyncMethod_SubscribeToTransportChanges<\n                     sushi_rpc::NotificationController::WithAsyncMethod_SubscribeToPropertyUpdates<\n                     sushi_rpc::NotificationController::WithAsyncMethod_SubscribeToAsyncCommandUpdates<\n                     sushi_rpc::NotificationController::Service\n                     >>>>>>>;\n\nclass NotificationControlService : public AsyncService,\n                                   private sushi::control::ControlListener\n{\npublic:\n    NotificationControlService(sushi::control::SushiControl* controller);\n\n    // Inherited from ControlListener\n    void notification(const sushi::control::ControlNotification* notification) override;\n\n    void subscribe(SubscribeToTransportChangesCallData* subscriber);\n    void unsubscribe(SubscribeToTransportChangesCallData* subscriber);\n\n    void subscribe(SubscribeToCpuTimingUpdatesCallData* subscriber);\n    void unsubscribe(SubscribeToCpuTimingUpdatesCallData* subscriber);\n\n    void subscribe(SubscribeToTrackChangesCallData* subscriber);\n    void unsubscribe(SubscribeToTrackChangesCallData* subscriber);\n\n    void subscribe(SubscribeToProcessorChangesCallData* subscriber);\n    void unsubscribe(SubscribeToProcessorChangesCallData* subscriber);\n\n    void subscribe(SubscribeToParameterUpdatesCallData* subscriber);\n    void unsubscribe(SubscribeToParameterUpdatesCallData* subscriber);\n\n    void subscribe(SubscribeToPropertyUpdatesCallData* subscriber);\n    void unsubscribe(SubscribeToPropertyUpdatesCallData* subscriber);\n\n    void subscribe(SubscribeToAsyncCommandUpdatesCallData* subscriber);\n    void unsubscribe(SubscribeToAsyncCommandUpdatesCallData* subscriber);\n\n    void delete_all_subscribers();\n\nprivate:\n    void _forward_transport_notification_to_subscribers(const sushi::control::ControlNotification* notification);\n    void _forward_cpu_timing_notification_to_subscribers(const sushi::control::ControlNotification* notification);\n    void _forward_track_notification_to_subscribers(const sushi::control::ControlNotification* notification);\n    void _forward_processor_notification_to_subscribers(const sushi::control::ControlNotification* notification);\n    void _forward_parameter_notification_to_subscribers(const sushi::control::ControlNotification* notification);\n    void _forward_property_notification_to_subscribers(const sushi::control::ControlNotification* notification);\n    void _forward_async_command_notification_to_subscribers(const sushi::control::ControlNotification* notification);\n\n    std::vector<SubscribeToTransportChangesCallData*> _transport_subscribers;\n    std::mutex _transport_subscriber_lock;\n\n    std::vector<SubscribeToCpuTimingUpdatesCallData*> _timing_subscribers;\n    std::mutex _timing_subscriber_lock;\n\n    std::vector<SubscribeToTrackChangesCallData*> _track_subscribers;\n    std::mutex _track_subscriber_lock;\n\n    std::vector<SubscribeToProcessorChangesCallData*> _processor_subscribers;\n    std::mutex _processor_subscriber_lock;\n\n    std::vector<SubscribeToParameterUpdatesCallData*> _parameter_subscribers;\n    std::mutex _parameter_subscriber_lock;\n\n    std::vector<SubscribeToPropertyUpdatesCallData*> _property_subscribers;\n    std::mutex _property_subscriber_lock;\n\n    std::vector<SubscribeToAsyncCommandUpdatesCallData*> _command_subscribers;\n    std::mutex _command_subscriber_lock;\n\n    sushi::control::SushiControl* _controller;\n\n    sushi::control::AudioGraphController* _audio_graph_controller;\n};\n\n}// sushi_rpc\n\n#endif //SUSHI_SUSHICONTROLSERVICE_H\n"
  },
  {
    "path": "rpc_interface/src/grpc_server.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief gRPC Server\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n#include <unordered_map>\n#include <vector>\n#include <string>\n\n#include \"control_service.h\"\n#include \"sushi_rpc/grpc_server.h\"\n#include \"async_service_call_data.h\"\n\nnamespace sushi_rpc {\n\nGrpcServer::GrpcServer(const std::string& listen_address,\n                       sushi::control::SushiControl* controller) : _listen_address{listen_address},\n                                                                   _system_control_service{std::make_unique<SystemControlService>(controller)},\n                                                                   _transport_control_service{std::make_unique<TransportControlService>(controller)},\n                                                                   _timing_control_service{std::make_unique<TimingControlService>(controller)},\n                                                                   _keyboard_control_service{std::make_unique<KeyboardControlService>(controller)},\n                                                                   _audio_graph_control_service{std::make_unique<AudioGraphControlService>(controller)},\n                                                                   _parameter_control_service{std::make_unique<ParameterControlService>(controller)},\n                                                                   _program_control_service{std::make_unique<ProgramControlService>(controller)},\n                                                                   _midi_control_service{std::make_unique<MidiControlService>(controller)},\n                                                                   _audio_routing_control_service{std::make_unique<AudioRoutingControlService>(controller)},\n                                                                   _osc_control_service{std::make_unique<OscControlService>(controller)},\n                                                                   _session_control_service{std::make_unique<SessionControlService>(controller)},\n                                                                   _notification_control_service{std::make_unique<NotificationControlService>(controller)},\n                                                                   _server_builder{std::make_unique<grpc::ServerBuilder>()},\n                                                                   _running{false}\n{}\n\nGrpcServer::~GrpcServer() = default;\n\nvoid GrpcServer::AsyncRpcLoop()\n{\n    new SubscribeToTransportChangesCallData(_notification_control_service.get(), _async_rpc_queue.get());\n    new SubscribeToCpuTimingUpdatesCallData(_notification_control_service.get(), _async_rpc_queue.get());\n    new SubscribeToTrackChangesCallData(_notification_control_service.get(), _async_rpc_queue.get());\n    new SubscribeToProcessorChangesCallData(_notification_control_service.get(), _async_rpc_queue.get());\n    new SubscribeToParameterUpdatesCallData(_notification_control_service.get(), _async_rpc_queue.get());\n    new SubscribeToPropertyUpdatesCallData(_notification_control_service.get(), _async_rpc_queue.get());\n    new SubscribeToAsyncCommandUpdatesCallData(_notification_control_service.get(), _async_rpc_queue.get());\n\n    while (_running.load())\n    {\n        void* tag;\n        bool ok;\n\n        _async_rpc_queue->Next(&tag, &ok);\n        if (ok == false)\n        {\n            static_cast<CallData*>(tag)->stop();\n        }\n        static_cast<CallData*>(tag)->proceed();\n    }\n}\n\nbool GrpcServer::start()\n{\n    _server_builder->AddChannelArgument(GRPC_ARG_ALLOW_REUSEPORT, 0);\n\n    _server_builder->AddListeningPort(_listen_address, grpc::InsecureServerCredentials());\n\n    _server_builder->RegisterService(_system_control_service.get());\n    _server_builder->RegisterService(_transport_control_service.get());\n    _server_builder->RegisterService(_timing_control_service.get());\n    _server_builder->RegisterService(_keyboard_control_service.get());\n    _server_builder->RegisterService(_audio_graph_control_service.get());\n    _server_builder->RegisterService(_parameter_control_service.get());\n    _server_builder->RegisterService(_program_control_service.get());\n    _server_builder->RegisterService(_midi_control_service.get());\n    _server_builder->RegisterService(_audio_routing_control_service.get());\n    _server_builder->RegisterService(_osc_control_service.get());\n    _server_builder->RegisterService(_session_control_service.get());\n    _server_builder->RegisterService(_notification_control_service.get());\n\n    _async_rpc_queue = _server_builder->AddCompletionQueue();\n    _server = _server_builder->BuildAndStart();\n\n    if (_server == nullptr)\n    {\n        return false;\n    }\n\n    _running.store(true);\n    _worker = std::thread(&GrpcServer::AsyncRpcLoop, this);\n\n    return true;\n}\n\nvoid GrpcServer::stop()\n{\n    if (_running == true)\n    {\n        auto now = std::chrono::system_clock::now();\n        _running.store(false);\n        _server->Shutdown(now + SERVER_SHUTDOWN_DEADLINE);\n        _async_rpc_queue->Shutdown();\n        if (_worker.joinable())\n        {\n            _worker.join();\n        }\n\n        void* tag;\n        bool ok;\n\n        // Empty completion queue\n        while (_async_rpc_queue->Next (&tag, &ok));\n        _notification_control_service->delete_all_subscribers();\n    }\n}\n\nvoid GrpcServer::waitForCompletion()\n{\n    _server->Wait();\n}\n\n} // sushi_rpc\n"
  },
  {
    "path": "src/audio_frontends/apple_coreaudio/apple_coreaudio_device.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief C++ representation of the AudioObject as used in the CoreAudio apis.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_APPLE_COREAUDIO_DEVICE_H\n#define SUSHI_APPLE_COREAUDIO_DEVICE_H\n\n\n#include \"elklog/static_logger.h\"\n#include \"apple_coreaudio_object.h\"\n\nnamespace apple_coreaudio {\n\n/**\n * This class represents a CoreAudio device as a thin C++ wrapper around AudioHardware.h APIs.\n */\nclass AudioDevice : public AudioObject\n{\npublic:\n    enum class Scope\n    {\n        UNDEFINED = 0,\n        INPUT,\n        OUTPUT,\n        INPUT_OUTPUT,\n    };\n\n    /**\n     * Baseclass for other classes who want to receive the audio callbacks from this device.\n     */\n    class AudioCallback\n    {\n    public:\n        /**\n         * Called when the device needs new audio data.\n         * @param input_data The audio input data.\n         * @param num_input_channels Number of input channels in the input buffer.\n         * @param output_data The audio output data.\n         * @param num_output_channels Number of output channels in the output buffer.\n         * @param input_host_time The time at which the input data was captured.\n         */\n        virtual void audio_callback([[maybe_unused]] const float* input_data,\n                                    [[maybe_unused]] int num_input_channels,\n                                    [[maybe_unused]] float* output_data,\n                                    [[maybe_unused]] int num_output_channels,\n                                    [[maybe_unused]] int num_frames,\n                                    [[maybe_unused]] uint64_t input_host_time) {}\n\n        /**\n         * Called when the device changed its sample rate.\n         * Warning! This call gets made from a random background thread, and there is no synchronisation or whatsoever.\n         * @param new_sample_rate The new sample rate of the device.\n         */\n        virtual void sample_rate_changed([[maybe_unused]] double new_sample_rate) {}\n    };\n\n    AudioDevice() : AudioObject(0) {}\n    explicit AudioDevice(AudioObjectID audio_object_id) : AudioObject(audio_object_id) {}\n\n    ~AudioDevice() override;\n\n    SUSHI_DECLARE_NON_COPYABLE(AudioDevice)\n\n    AudioDevice(AudioDevice&& other) noexcept : AudioObject(0)\n    {\n        *this = std::move(other); // Call into move assignment operator.\n    }\n\n    AudioDevice& operator=(AudioDevice&& other) noexcept;\n\n    /**\n     * Starts IO on this device.\n     * @return True if successful, or false if an error occurred.\n     */\n    bool start_io(apple_coreaudio::AudioDevice::AudioCallback* audio_callback);\n\n    /**\n     * Stops IO on this device.\n     * @return True if successful, or false if an error occurred.\n     */\n    bool stop_io();\n\n    /**\n     * @return The name of the device.\n     */\n    [[nodiscard]] std::string name() const;\n\n    /**\n     * Gets the name of the device. When the device is an aggregate there will be different names\n     * for the input device and output device, hence the ability to choose the scope.\n     * @param scope The scope for which to get the name for.\n     * @return The name of the device.\n     */\n    [[nodiscard]] virtual std::string name(Scope scope) const;\n\n    /**\n     * Returns the UID of this device. The UID is persistent across system boots and cannot be shared with other systems.\n     * For more information, read the documentation of kAudioDevicePropertyDeviceUID inside AudioHardware.h\n     * @return The UID of the device.\n     */\n    [[nodiscard]] std::string uid() const;\n\n    /**\n     * @param for_input When true the amount of channels for the input will be returned, when false the amount\n     * of channels for the output will be returned.\n     * @return The amount of channels for the input or output.\n     */\n    [[nodiscard]] virtual int num_channels(bool for_input) const;\n\n    /**\n     * @param for_input True to get the number of input streams, or false to get the number of output streams.\n     * @return The number of streams, or -1 if an error occurred.\n     */\n    [[nodiscard]] size_t num_streams(bool for_input) const;\n\n    /**\n     * @param buffer_frame_size The number of frames in the io buffers.\n     * @return True if succeeded, or false if buffer frame size could not be set.\n     */\n    [[nodiscard]] bool set_buffer_frame_size(uint32_t buffer_frame_size) const;\n\n    /**\n     * Sets the sample rate of this device\n     * @param sample_rate The new sample rate. Apple's API seems to accept a value with a max deviation of 0.000000000001.\n     * @return True if setting the sample rate succeeded, or false if an error occurred.\n     */\n    [[nodiscard]] bool set_nominal_sample_rate(double sample_rate) const;\n\n    /**\n     * Gets the nominal sample rate of this device.\n     * @return The nominal sample rate, or 0.0 if an error occurred.\n     */\n    [[nodiscard]] double nominal_sample_rate() const;\n\n    /**\n     * @return Returns an array with all possible nominal sample rates the device can run at.\n     */\n    [[nodiscard]] std::vector<double> available_nominal_sample_rates() const;\n\n    /**\n     * @return Returns the available buffer sizes for this device as a AudioValueRange with a minimum and maximum value.\n     */\n    [[nodiscard]] AudioValueRange available_buffer_sizes() const;\n\n    /**\n     * @param for_input True for input or false for output.\n     * @return The device latency in samples. Note that stream latency must be added to this number in order to get the total latency.\n     */\n    [[nodiscard]] UInt32 device_latency(bool for_input) const;\n\n    /**\n     *\n     * @param for_input True to get the latency for the selected input stream, or false to get the latency for the selected output stream.\n     * @return The latency of the selected stream in samples, or 0 if the stream for index does not exist.\n     */\n    [[nodiscard]] UInt32 selected_stream_latency(bool for_input) const;\n\n    /**\n     * @param stream_index The index of the stream to get the latency for.\n     * @return The latency of the stream for given index in samples, or 0 if the stream for index does not exist.\n     */\n    [[nodiscard]] UInt32 stream_latency(size_t stream_index, bool for_input) const;\n\n    /**\n     * @return An UInt32 whose value indicates to which clock domain this device belongs.\n     * All devices with the same value belong to the same clock domain.\n     * A value of 0 means no information about the clock domain is given.\n     */\n    [[nodiscard]] UInt32 clock_domain_id() const;\n\n    /**\n     * @return A list of AudioObjectIDs of devices which are related to this device.\n     * AudioDevices are related if they share the same IOAudioDevice object.\n     */\n    [[nodiscard]] std::vector<UInt32> related_devices() const;\n\n    /**\n     * Creates an aggregate device from given input- and output device.\n     * This aggregate device is opinionated in the sense that the input channels of the input_device will be used\n     * as input and the output channels of the output device as output. This discards the the output channels\n     * of the input device and the input channels of the output device.\n     * Said otherwise, while an aggregate device normally can have many sub devices, this particular instance will only have 2.\n     * @param input_device The device to use as intput.\n     * @param output_device The device to use as output.\n     * @return An aggregate device, or nullptr if an error occurred.\n     */\n    static std::unique_ptr<AudioDevice> create_aggregate_device(const AudioDevice& input_device, const AudioDevice& output_device);\n\n    /**\n     * @return True if this audio device is an aggregate device, or false if not.\n     */\n    [[nodiscard]] bool is_aggregate_device() const;\n\nprotected:\n    void property_changed(const AudioObjectPropertyAddress& address) override;\n\n    /**\n     * Selects an input or output stream.\n     * Note: when this is an aggregate device the number of streams will be the total of all streams of all devices.\n     * @param for_input True to select an input stream, or false to select an output stream.\n     * @param selected_stream_index The index of the stream to select.\n     */\n    void select_stream(bool for_input, size_t selected_stream_index);\n\nprivate:\n    /**\n     * Static function which gets called by an audio device to provide and get audio data.\n     * @return The return value is currently unused and should always be 0 (see AudioDeviceIOProc in AudioHardware.h).\n     */\n    static OSStatus _audio_device_io_proc(AudioObjectID audio_object_id,\n                                          const AudioTimeStamp* now,\n                                          const AudioBufferList* input_data,\n                                          const AudioTimeStamp* input_time,\n                                          AudioBufferList* output_data,\n                                          const AudioTimeStamp* output_time,\n                                          void* client_data);\n\n    /**\n     * @param for_input True to get input streams, or false to get output streams.\n     * @return An array with AudioObjectIDs of streams for given input.\n     */\n    [[nodiscard]] std::vector<UInt32> _stream_ids(bool for_input) const;\n\n    /// Holds the identifier for the io proc audio callbacks.\n    AudioDeviceIOProcID _io_proc_id{nullptr};\n    AudioCallback* _audio_callback{nullptr};\n\n    size_t _selected_input_stream_index{0};\n    size_t _selected_output_stream_index{0};\n};\n\n/**\n * Tries to find an audio device with given UID.\n * @param audio_devices The list of audio device to search in.\n * @param uid The UID of the device to find.\n * @return Pointer to the found device, or nullptr if no device with given UID was found.\n */\nconst AudioDevice* device_for_uid(const std::vector<AudioDevice>& audio_devices, const std::string& uid);\n\n} // namespace apple_coreaudio\n\n#endif // SUSHI_APPLE_COREAUDIO_DEVICE_H\n"
  },
  {
    "path": "src/audio_frontends/apple_coreaudio/apple_coreaudio_device.mm",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include \"apple_coreaudio_device.h\"\n\n#include <Foundation/NSString.h>\n#include <Foundation/NSDictionary.h>\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"AppleCoreAudio\");\n\nnamespace apple_coreaudio {\n\n/**\n * A class which represents a Core Audio Aggregate Device. This aggregate can have 2 devices, one for input and one for output.\n * The main reason for the existence of this class is to allow passing audio between 2 different devices.\n * Different devices often have different clocks, and a slightly different sample rate, even when the nominal rates are equal.\n * To pass audio between these devices you'd have to add async SRC to account for drift. To avoid doing that manually, this class\n * uses the Core Audio aggregate device API which handles the drift correction transparently.\n */\nclass AggregateAudioDevice : public AudioDevice\n{\npublic:\n    explicit AggregateAudioDevice(AudioObjectID audio_object_id) : AudioDevice(audio_object_id)\n    {\n        auto sub_devices = get_property_array<UInt32>({kAudioAggregateDevicePropertyActiveSubDeviceList,\n                                                       kAudioObjectPropertyScopeGlobal,\n                                                       kAudioObjectPropertyElementMain});\n\n        if (!is_aggregate_device())\n        {\n            ELKLOG_LOG_ERROR(\"AudioDevice is not an aggregate\");\n            return;\n        }\n\n        if (sub_devices.size() < 2)\n        {\n            ELKLOG_LOG_ERROR(\"Not enough sub devices available\");\n            return;\n        }\n\n        _input_device = AudioDevice(sub_devices[0]);\n        _output_device = AudioDevice(sub_devices[1]);\n\n        select_stream(true, 0);                                 // Select the first input stream of the input device.\n        select_stream(false, _input_device.num_streams(false)); // Select the first output stream of the output device.\n    }\n\n    ~AggregateAudioDevice() override\n    {\n        stop_io();\n        CA_LOG_IF_ERROR(AudioHardwareDestroyAggregateDevice(get_audio_object_id()));\n    }\n\n    [[nodiscard]] std::string name(Scope scope) const override\n    {\n        switch (scope)\n        {\n            case Scope::INPUT:\n                return _input_device.name();\n            case Scope::OUTPUT:\n                return _output_device.name();\n            case Scope::INPUT_OUTPUT:\n            case Scope::UNDEFINED:\n                return _input_device.name() + \" / \" + _output_device.name();\n        }\n    }\n\n    [[nodiscard]] int num_channels(bool for_input) const override\n    {\n        return for_input ? _input_device.num_channels(true) : _output_device.num_channels(false);\n    }\n\nprivate:\n    AudioDevice _input_device;\n    AudioDevice _output_device;\n};\n\n} // namespace apple_coreaudio\n\napple_coreaudio::AudioDevice::~AudioDevice()\n{\n    stop_io();\n}\n\nbool apple_coreaudio::AudioDevice::start_io(apple_coreaudio::AudioDevice::AudioCallback* audio_callback)\n{\n    if (!is_valid() || _io_proc_id != nullptr || audio_callback == nullptr)\n    {\n        return false;\n    }\n\n    _audio_callback = audio_callback;\n\n    CA_RETURN_IF_ERROR(AudioDeviceCreateIOProcID(get_audio_object_id(), _audio_device_io_proc, this, &_io_proc_id), false);\n    CA_RETURN_IF_ERROR(AudioDeviceStart(get_audio_object_id(), _io_proc_id), false);\n\n    if (!add_property_listener({kAudioDevicePropertyNominalSampleRate,\n                                kAudioObjectPropertyScopeGlobal,\n                                kAudioObjectPropertyElementMain}))\n    {\n        ELKLOG_LOG_ERROR(\"Failed to install property listener for sample rate change\");\n    }\n\n    return true;\n}\n\nbool apple_coreaudio::AudioDevice::stop_io()\n{\n    if (!is_valid() || _io_proc_id == nullptr)\n    {\n        return false;\n    }\n\n    CA_LOG_IF_ERROR(AudioDeviceStop(get_audio_object_id(), _io_proc_id));\n    CA_LOG_IF_ERROR(AudioDeviceDestroyIOProcID(get_audio_object_id(), _io_proc_id));\n\n    _io_proc_id = nullptr;\n    _audio_callback = nullptr;\n\n    return true;\n}\n\nstd::string apple_coreaudio::AudioDevice::name() const\n{\n    if (!is_valid())\n    {\n        return {};\n    }\n\n    return get_cfstring_property({kAudioObjectPropertyName,\n                                  kAudioObjectPropertyScopeGlobal,\n                                  kAudioObjectPropertyElementMain});\n}\n\nstd::string apple_coreaudio::AudioDevice::name(apple_coreaudio::AudioDevice::Scope) const\n{\n    return name();\n}\n\nstd::string apple_coreaudio::AudioDevice::uid() const\n{\n    if (!is_valid())\n    {\n        return {};\n    }\n\n    return get_cfstring_property({kAudioDevicePropertyDeviceUID,\n                                  kAudioObjectPropertyScopeGlobal,\n                                  kAudioObjectPropertyElementMain});\n}\n\nint apple_coreaudio::AudioDevice::num_channels(bool for_input) const\n{\n    if (!is_valid())\n    {\n        return -1;\n    }\n\n    AudioObjectPropertyAddress pa{kAudioDevicePropertyStreamConfiguration,\n                                  for_input ? kAudioObjectPropertyScopeInput : kAudioObjectPropertyScopeOutput,\n                                  kAudioObjectPropertyElementMain};\n\n    if (!has_property(pa))\n    {\n        return -1;\n    }\n\n    auto data_size = get_property_data_size(pa);\n\n    if (data_size == 0)\n    {\n        return -1;\n    }\n\n    // Use std::vector as underlying storage so that the allocated memory is under RAII.\n    std::vector<uint8_t> storage(data_size);\n\n    AudioBufferList* audio_buffer_list;\n    audio_buffer_list = reinterpret_cast<AudioBufferList*>(storage.data());\n\n    if (get_property_data(pa, data_size, audio_buffer_list) != data_size)\n    {\n        ELKLOG_LOG_ERROR(\"Invalid data returned\");\n        return -1;\n    }\n\n    if (audio_buffer_list->mNumberBuffers == 0)\n    {\n        return 0;\n    }\n\n    auto selected_stream_index = for_input ? _selected_input_stream_index : _selected_output_stream_index;\n    if (selected_stream_index >= audio_buffer_list->mNumberBuffers)\n    {\n        ELKLOG_LOG_ERROR(\"Invalid stream index\");\n        return -1;\n    }\n\n    UInt32 channel_count = audio_buffer_list->mBuffers[selected_stream_index].mNumberChannels;\n\n    if (channel_count > std::numeric_limits<int>::max())\n    {\n        ELKLOG_LOG_ERROR(\"Integer overflow\");\n        return -1;\n    }\n\n    return static_cast<int>(channel_count);\n}\n\nsize_t apple_coreaudio::AudioDevice::num_streams(bool for_input) const\n{\n    return _stream_ids(for_input).size();\n}\n\nbool apple_coreaudio::AudioDevice::set_buffer_frame_size(uint32_t buffer_frame_size) const\n{\n    if (!is_valid())\n    {\n        return false;\n    }\n\n    AudioObjectPropertyAddress pa{kAudioDevicePropertyBufferFrameSize,\n                                  kAudioObjectPropertyScopeGlobal,\n                                  kAudioObjectPropertyElementMain};\n\n    return set_property(pa, buffer_frame_size);\n}\n\nbool apple_coreaudio::AudioDevice::set_nominal_sample_rate(double sample_rate) const\n{\n    if (!is_valid())\n    {\n        return false;\n    }\n\n    AudioObjectPropertyAddress pa{kAudioDevicePropertyNominalSampleRate,\n                                  kAudioObjectPropertyScopeGlobal,\n                                  kAudioObjectPropertyElementMain};\n\n    return set_property(pa, sample_rate);\n}\n\ndouble apple_coreaudio::AudioDevice::nominal_sample_rate() const\n{\n    if (!is_valid())\n    {\n        return 0.0;\n    }\n\n    AudioObjectPropertyAddress pa{kAudioDevicePropertyNominalSampleRate,\n                                  kAudioObjectPropertyScopeGlobal,\n                                  kAudioObjectPropertyElementMain};\n\n    return get_property<double>(pa);\n}\n\nstd::vector<double> apple_coreaudio::AudioDevice::available_nominal_sample_rates() const\n{\n    if (!is_valid())\n    {\n        return {};\n    }\n\n    auto rates = get_property_array<AudioValueRange>({kAudioDevicePropertyAvailableNominalSampleRates,\n                                                      kAudioObjectPropertyScopeGlobal,\n                                                      kAudioObjectPropertyElementMain});\n\n    std::vector<double> available_sample_rates;\n    available_sample_rates.reserve(rates.size());\n\n    for (auto& rate : rates)\n    {\n        assert(rate.mMinimum == rate.mMaximum);\n        available_sample_rates.push_back(rate.mMaximum);\n    }\n\n    return available_sample_rates;\n}\n\nAudioValueRange apple_coreaudio::AudioDevice::available_buffer_sizes() const\n{\n    return get_property<AudioValueRange>({kAudioDevicePropertyBufferFrameSizeRange,\n                                          kAudioObjectPropertyScopeGlobal,\n                                          kAudioObjectPropertyElementMain});\n}\n\nUInt32 apple_coreaudio::AudioDevice::device_latency(bool for_input) const\n{\n    if (!is_valid())\n    {\n        return 0;\n    }\n\n    AudioObjectPropertyAddress pa{kAudioDevicePropertyLatency,\n                                  for_input ? kAudioObjectPropertyScopeInput : kAudioObjectPropertyScopeOutput,\n                                  kAudioObjectPropertyElementMain};\n\n    return get_property<UInt32>(pa);\n}\n\nstd::vector<UInt32> apple_coreaudio::AudioDevice::_stream_ids(bool for_input) const\n{\n    return get_property_array<UInt32>({kAudioDevicePropertyStreams,\n                                       for_input ? kAudioObjectPropertyScopeInput\n                                                 : kAudioObjectPropertyScopeOutput,\n                                       kAudioObjectPropertyElementMain});\n}\n\nUInt32 apple_coreaudio::AudioDevice::stream_latency(size_t stream_index, bool for_input) const\n{\n    if (!is_valid())\n    {\n        return 0;\n    }\n\n    auto stream_ids = _stream_ids(for_input);\n\n    if (stream_index >= stream_ids.size())\n    {\n        ELKLOG_LOG_ERROR(\"Stream for index {} does not exist\", stream_index);\n        return 0;\n    }\n\n    return AudioObject::get_property<UInt32>(stream_ids[stream_index], {kAudioStreamPropertyLatency,\n                                                                        for_input ? kAudioObjectPropertyScopeInput : kAudioObjectPropertyScopeOutput,\n                                                                        kAudioObjectPropertyElementMain});\n}\n\nUInt32 apple_coreaudio::AudioDevice::selected_stream_latency(bool for_input) const\n{\n    return stream_latency(for_input ? _selected_input_stream_index : _selected_output_stream_index, for_input);\n}\n\nUInt32 apple_coreaudio::AudioDevice::clock_domain_id() const\n{\n    if (!is_valid())\n    {\n        return 0;\n    }\n\n    return get_property<UInt32>({kAudioDevicePropertyClockDomain,\n                                 kAudioObjectPropertyScopeGlobal,\n                                 kAudioObjectPropertyElementMain});\n}\n\nstd::vector<UInt32> apple_coreaudio::AudioDevice::related_devices() const\n{\n    auto ids = get_property_array<UInt32>({kAudioDevicePropertyRelatedDevices,\n                                           kAudioObjectPropertyScopeGlobal,\n                                           kAudioObjectPropertyElementMain});\n    return ids;\n}\n\nvoid apple_coreaudio::AudioDevice::property_changed(const AudioObjectPropertyAddress& address)\n{\n    // Note: this function most likely gets called from a background thread (most likely because there is no official specification on this).\n\n    // Nominal sample rate\n    if (address == AudioObjectPropertyAddress{kAudioDevicePropertyNominalSampleRate,\n                                              kAudioObjectPropertyScopeGlobal,\n                                              kAudioObjectPropertyElementMain})\n    {\n        if (_audio_callback)\n        {\n            _audio_callback->sample_rate_changed(nominal_sample_rate());\n        }\n    }\n}\n\nOSStatus apple_coreaudio::AudioDevice::_audio_device_io_proc(AudioObjectID audio_object_id,\n                                                             const AudioTimeStamp*,\n                                                             const AudioBufferList* input_data,\n                                                             const AudioTimeStamp* input_time,\n                                                             AudioBufferList* output_data,\n                                                             const AudioTimeStamp*,\n                                                             void* client_data)\n{\n    auto* audio_device = reinterpret_cast<AudioDevice*>(client_data);\n    if (audio_device == nullptr)\n    {\n        return 0;\n    }\n\n    if (audio_object_id != audio_device->get_audio_object_id())\n    {\n        return 0; // Wrong audio object id.\n    }\n\n    if (audio_device->_audio_callback == nullptr)\n    {\n        return 0; // No audio callback installed.\n    }\n\n    if (output_data == nullptr)\n    {\n        return 0;\n    }\n\n    // Clear output buffers.\n    for (UInt32 i = 0; i < output_data->mNumberBuffers; i++)\n    {\n        std::memset(output_data->mBuffers[i].mData, 0, output_data->mBuffers[i].mDataByteSize);\n    }\n\n    // Do this check after the output buffers have been cleared.\n    if (input_data == nullptr)\n    {\n        return 0;\n    }\n\n    auto input_stream_index = audio_device->_selected_input_stream_index;\n    auto output_stream_index = audio_device->_selected_output_stream_index;\n\n    if (input_data->mNumberBuffers <= input_stream_index || output_data->mNumberBuffers <= output_stream_index)\n    {\n        return 0;\n    }\n\n    auto input_frame_count = static_cast<int32_t>(input_data->mBuffers[input_stream_index].mDataByteSize / input_data->mBuffers[input_stream_index].mNumberChannels / sizeof(float));\n    auto output_frame_count = static_cast<int32_t>(output_data->mBuffers[output_stream_index].mDataByteSize / output_data->mBuffers[output_stream_index].mNumberChannels / sizeof(float));\n\n    assert(input_frame_count == sushi::AUDIO_CHUNK_SIZE);\n    assert(input_frame_count == output_frame_count);\n\n    audio_device->_audio_callback->audio_callback(static_cast<const float*>(input_data->mBuffers[input_stream_index].mData),\n                                                  static_cast<int>(input_data->mBuffers[input_stream_index].mNumberChannels),\n                                                  static_cast<float*>(output_data->mBuffers[output_stream_index].mData),\n                                                  static_cast<int>(output_data->mBuffers[output_stream_index].mNumberChannels),\n                                                  std::min(input_frame_count, output_frame_count),\n                                                  input_time->mHostTime);\n\n    return 0;\n}\n\nstd::unique_ptr<apple_coreaudio::AudioDevice> apple_coreaudio::AudioDevice::create_aggregate_device(const AudioDevice& input_device,\n                                                                                                    const AudioDevice& output_device)\n{\n    if (!input_device.is_valid() || !output_device.is_valid())\n    {\n        return nullptr;\n    }\n\n    if (input_device.is_aggregate_device())\n    {\n        ELKLOG_LOG_ERROR(\"Input device \\\"{}\\\" is an aggregate device which cannot be part of another aggregate device\",\n                        input_device.name());\n        return nullptr;\n    }\n\n    if (output_device.is_aggregate_device())\n    {\n        ELKLOG_LOG_ERROR(\"Output device \\\"{}\\\" is an aggregate device which cannot be part of another aggregate device\",\n                        output_device.name());\n        return nullptr;\n    }\n\n    NSString* input_uid = [NSString stringWithUTF8String:input_device.uid().c_str()];\n    NSString* output_uid = [NSString stringWithUTF8String:output_device.uid().c_str()];\n\n    NSDictionary* description = @{\n        @(kAudioAggregateDeviceUIDKey): @\"audio.elk.sushi.aggregate\",\n        @(kAudioAggregateDeviceIsPrivateKey): @(1),\n        @(kAudioAggregateDeviceSubDeviceListKey): @[\n            @{\n                @(kAudioSubDeviceUIDKey): input_uid,\n                @(kAudioSubDeviceDriftCompensationKey): @(0),\n                @(kAudioSubDeviceDriftCompensationQualityKey): @(kAudioSubDeviceDriftCompensationMinQuality),\n            },\n            @{\n                @(kAudioSubDeviceUIDKey): output_uid,\n                @(kAudioSubDeviceDriftCompensationKey): @(1),\n                @(kAudioSubDeviceDriftCompensationQualityKey): @(kAudioSubDeviceDriftCompensationMinQuality),\n            },\n        ],\n    };\n\n    AudioObjectID aggregate_device_id{0};\n    OSStatus status = AudioHardwareCreateAggregateDevice((CFDictionaryRef) description, &aggregate_device_id);\n    if (status != noErr)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to create aggregate device\");\n        return nullptr;\n    }\n\n    auto aggregate_device = std::make_unique<AggregateAudioDevice>(aggregate_device_id);\n\n    auto device = apple_coreaudio::AudioDevice(aggregate_device_id);\n\n    return aggregate_device;\n}\n\napple_coreaudio::AudioDevice& apple_coreaudio::AudioDevice::operator=(apple_coreaudio::AudioDevice&& other) noexcept\n{\n    // Since we're going to adopt another AudioDeviceID we must stop any audio IO proc.\n    stop_io();\n\n    // Don't transfer ownership of _io_proc_id because CoreAudio has registered the pointer\n    // to other as client data, so let other stop the callbacks when it goes out of scope.\n    AudioObject::operator=(std::move(other));\n    return *this;\n}\n\nbool apple_coreaudio::AudioDevice::is_aggregate_device() const\n{\n    return get_property<UInt32>({kAudioObjectPropertyClass,\n                                 kAudioObjectPropertyScopeGlobal,\n                                 kAudioObjectPropertyElementMain}) == kAudioAggregateDeviceClassID;\n}\n\nvoid apple_coreaudio::AudioDevice::select_stream(bool for_input, size_t selected_stream_index)\n{\n    if (selected_stream_index >= num_streams(for_input))\n    {\n        return;\n    }\n\n    for_input ? _selected_input_stream_index = selected_stream_index : _selected_output_stream_index = selected_stream_index;\n}\n\nconst apple_coreaudio::AudioDevice* apple_coreaudio::device_for_uid(const std::vector<AudioDevice>& audio_devices,\n                                                                    const std::string& uid)\n{\n    for (auto& device : audio_devices)\n    {\n        if (device.uid() == uid)\n        {\n            return &device;\n        }\n    }\n    return nullptr;\n}\n"
  },
  {
    "path": "src/audio_frontends/apple_coreaudio/apple_coreaudio_object.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"apple_coreaudio_object.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"AppleCoreAudio\");\n\nnamespace apple_coreaudio {\n\nAudioObject::~AudioObject()\n{\n    // Remove property listeners\n    for (auto& listener_address : _property_listeners)\n    {\n        CA_LOG_IF_ERROR(AudioObjectRemovePropertyListener(_audio_object_id, &listener_address, &_audio_object_property_listener_proc, this));\n    }\n}\n\nAudioObjectID AudioObject::get_audio_object_id() const\n{\n    return _audio_object_id;\n}\n\nbool AudioObject::is_valid() const\n{\n    return _audio_object_id != 0;\n}\n\nbool AudioObject::has_property(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address)\n{\n    return AudioObjectHasProperty(audio_object_id, &address);\n}\n\nUInt32 AudioObject::get_property_data_size(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address)\n{\n    UInt32 data_size = 0;\n    CA_RETURN_IF_ERROR(AudioObjectGetPropertyDataSize(audio_object_id, &address, 0, nullptr, &data_size), 0);\n    return data_size;\n}\n\nbool AudioObject::is_property_settable(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address)\n{\n    Boolean is_settable = false;\n    CA_RETURN_IF_ERROR(AudioObjectIsPropertySettable(audio_object_id, &address, &is_settable), false);\n    return is_settable != 0;\n}\n\nUInt32 AudioObject::get_property_data(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address, UInt32 data_size, void* data)\n{\n    UInt32 io_data_size = data_size;\n    CA_RETURN_IF_ERROR(AudioObjectGetPropertyData(audio_object_id, &address, 0, nullptr, &io_data_size, data), 0);\n    return io_data_size;\n}\n\nbool AudioObject::set_property_data(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address, UInt32 data_size, const void* data)\n{\n    CA_RETURN_IF_ERROR(AudioObjectSetPropertyData(audio_object_id, &address, 0, nullptr, data_size, data), false);\n    return true;\n}\n\nstd::string AudioObject::get_cfstring_property(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address)\n{\n    const auto* cf_string_ref = get_property<CFStringRef>(audio_object_id, address);\n    if (cf_string_ref == nullptr)\n    {\n        return {};\n    }\n\n    auto string = apple_coreaudio::cf_string_to_std_string(cf_string_ref);\n\n    CFRelease(cf_string_ref);\n\n    return string;\n}\n\nstd::string AudioObject::get_cfstring_property(const AudioObjectPropertyAddress& address) const\n{\n    return get_cfstring_property(_audio_object_id, address);\n}\n\nbool AudioObject::has_property(const AudioObjectPropertyAddress& address) const\n{\n    return AudioObjectHasProperty(_audio_object_id, &address);\n}\n\nbool AudioObject::is_property_settable(const AudioObjectPropertyAddress& address) const\n{\n    return is_property_settable(_audio_object_id, address);\n}\n\nUInt32 AudioObject::get_property_data_size(const AudioObjectPropertyAddress& address) const\n{\n    return get_property_data_size(_audio_object_id, address);\n}\n\nUInt32 AudioObject::get_property_data(const AudioObjectPropertyAddress& address, UInt32 data_size, void* data) const\n{\n    return get_property_data(_audio_object_id, address, data_size, data);\n}\n\nbool AudioObject::set_property_data(const AudioObjectPropertyAddress& address, UInt32 data_size, const void* data) const\n{\n    return set_property_data(_audio_object_id, address, data_size, data);\n}\n\nbool AudioObject::add_property_listener(const AudioObjectPropertyAddress& address)\n{\n    for (const auto& listener_address : _property_listeners)\n    {\n        if (listener_address == address)\n            return true;\n    }\n\n    CA_RETURN_IF_ERROR(AudioObjectAddPropertyListener(_audio_object_id, &address, &_audio_object_property_listener_proc, this), false);\n\n    _property_listeners.push_back(address);\n\n    return true;\n}\n\nOSStatus AudioObject::_audio_object_property_listener_proc(AudioObjectID audio_object_id, UInt32 num_addresses, const AudioObjectPropertyAddress* address, void* client_data)\n{\n    if (address == nullptr || client_data == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Invalid object passed to _audio_object_property_listener_proc\");\n        return kAudioHardwareBadObjectError;\n    }\n\n    auto* audio_object = static_cast<AudioObject*>(client_data);\n\n    if (audio_object_id != audio_object->_audio_object_id)\n    {\n        ELKLOG_LOG_ERROR(\"AudioObjectID mismatch (in _audio_object_property_listener_proc)\");\n\n        ELKLOG_LOG_ERROR(\"AudioObjectID mismatch (in _audio_object_property_listener_proc)\");\n\n        return kAudioHardwareBadObjectError;\n    }\n\n    for (UInt32 i = 0; i < num_addresses; i++)\n    {\n        audio_object->property_changed(address[i]);\n    }\n\n    return kAudioHardwareNoError;\n}\n\ntemplate<typename T>\nstd::vector<T> AudioObject::get_property_array(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address)\n{\n    std::vector<T> data_array;\n    AudioObject::get_property_array(audio_object_id, address, data_array);\n    return data_array;\n}\n\ntemplate<typename T>\nbool AudioObject::get_property_array(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address, std::vector<T>& data_array)\n{\n    data_array.resize(0);\n\n    if (!has_property(audio_object_id, address))\n    {\n        ELKLOG_LOG_ERROR(\"AudioObject doesn't have requested property\");\n        return false;\n    }\n\n    auto data_size = get_property_data_size(audio_object_id, address);\n\n    if (data_size == 0)\n    {\n        return true; // No data available.\n    }\n\n    if (data_size % sizeof(T) != 0)\n    {\n        ELKLOG_LOG_ERROR(\"Invalid array property size\");\n        return false;\n    }\n\n    auto num_elements = data_size / sizeof(T);\n    data_array.resize(num_elements);\n\n    auto num_bytes = data_array.size() * sizeof(T);\n\n    data_size = get_property_data(audio_object_id, address, static_cast<UInt32>(num_bytes), data_array.data());\n\n    // Resize array based on what we actually got.\n    data_array.resize(data_size / sizeof(T));\n\n    return true;\n}\n\n// Force implementations for these types to be generated, so we can log from this function (which is not possible in the header file).\ntemplate std::vector<UInt32> AudioObject::get_property_array(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address);\ntemplate std::vector<AudioValueRange> AudioObject::get_property_array(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address);\n\ntemplate<typename T>\nbool AudioObject::set_property(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address, const T& value)\n{\n    if (!has_property(audio_object_id, address))\n    {\n        ELKLOG_LOG_ERROR(\"AudioObject doesn't have requested property\");\n        return false;\n    }\n\n    if (!is_property_settable(audio_object_id, address))\n    {\n        ELKLOG_LOG_ERROR(\"Property is not settable\");\n        return false;\n    }\n\n    const auto type_size = sizeof(T); // NOLINT Clang-Tidy: Suspicious usage of 'sizeof(A*)'; pointer to aggregate\n\n    if (get_property_data_size(audio_object_id, address) != type_size)\n    {\n        ELKLOG_LOG_ERROR(\"AudioObject's property size invalid\");\n        return false;\n    }\n\n    return set_property_data(audio_object_id, address, type_size, &value);\n}\n\n// Force implementations for these types to be generated, so we can log from this function (which is not possible in the header file).\ntemplate bool AudioObject::set_property(AudioObjectID, const AudioObjectPropertyAddress&, const double&);\ntemplate bool AudioObject::set_property(AudioObjectID, const AudioObjectPropertyAddress&, const UInt32&);\n\ntemplate<typename T>\nT AudioObject::get_property(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address)\n{\n    if (!has_property(audio_object_id, address))\n    {\n        ELKLOG_LOG_ERROR(\"AudioObject doesn't have requested property\");\n        return {};\n    }\n\n    const auto type_size = sizeof(T); // NOLINT Clang-Tidy: Suspicious usage of 'sizeof(A*)'; pointer to aggregate\n\n    if (get_property_data_size(audio_object_id, address) != type_size)\n    {\n        ELKLOG_LOG_ERROR(\"AudioObject's property size invalid\");\n        return {};\n    }\n\n    T data{};\n    auto data_size = get_property_data(audio_object_id, address, type_size, &data);\n    if (data_size != type_size)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to get data from AudioObject\");\n        return {};\n    }\n\n    return data;\n}\n\n// Force implementations for these types to be generated, so we can log from this function (which is not possible in the header file).\ntemplate double AudioObject::get_property<double>(AudioObjectID, const AudioObjectPropertyAddress&);\ntemplate UInt32 AudioObject::get_property<UInt32>(AudioObjectID, const AudioObjectPropertyAddress&);\ntemplate AudioValueRange AudioObject::get_property<AudioValueRange>(AudioObjectID, const AudioObjectPropertyAddress&);\n\n} // namespace apple_coreaudio\n"
  },
  {
    "path": "src/audio_frontends/apple_coreaudio/apple_coreaudio_object.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief C++ representation of the AudioObject as used in the CoreAudio apis.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_APPLE_COREAUDIO_OBJECT_H\n#define SUSHI_APPLE_COREAUDIO_OBJECT_H\n\n#include <vector>\n\n#include \"sushi/constants.h\"\n#include \"apple_coreaudio_utils.h\"\n\nnamespace apple_coreaudio {\n\n/**\n * This class represents a numerical audio object as we know from the Core Audio API (AudioHardware.h etc).\n * It also implements basic, common capabilities of an audio object, like getting and setting of properties.\n */\nclass AudioObject\n{\npublic:\n    AudioObject() = delete;\n\n    explicit AudioObject(AudioObjectID audio_object_id) : _audio_object_id(audio_object_id)\n    {\n    }\n\n    virtual ~AudioObject();\n\n    SUSHI_DECLARE_NON_COPYABLE(AudioObject)\n\n    AudioObject(AudioObject&& other) noexcept\n    {\n        *this = std::move(other); // Call into move assignment operator.\n    }\n\n    AudioObject& operator=(AudioObject&& other) noexcept\n    {\n        std::swap(_property_listeners, other._property_listeners);\n\n        _audio_object_id = other._audio_object_id;\n        other._audio_object_id = 0;\n\n        return *this;\n    }\n\n    /**\n     * @return The AudioObjectID for this AudioObject.\n     */\n    [[nodiscard]] AudioObjectID get_audio_object_id() const;\n\n    /**\n     * @return True if this object represents an actual object, or false if the audio object id is 0.\n     */\n    [[nodiscard]] bool is_valid() const;\n\n    /**\n     * Tests if this AudioObject has property for given address.\n     * @param audio_object_id The ID of the AudioObject to query.\n     * @param address The address of the property to lookup.\n     * @return True if this object has the property, or false if not.\n     */\n    static bool has_property(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address);\n\n    /**\n     * Gets the data for property of type T.\n     * @tparam T The type of the property (it's size must match the size of the property).\n     * @param audio_object_id The ID of the AudioObject to query.\n     * @param address The address of the property.\n     * @return The property, or a default constructed value on error.\n     */\n    template<typename T>\n    static T get_property(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address);\n\n    /**\n     * Sets the data for property of type T.\n     * @tparam T The type of the property (it's size must match the size of the property).\n     * @param address The address of the property.\n     * @param audio_object_id The ID of the AudioObject to set the property for.\n     * @param value The value to set.\n     * @return True if successful, or false if property could not be set.\n     */\n    template<typename T>\n    static bool set_property(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address, const T& value);\n\n    /**\n     * Gets an array property.\n     * @tparam T The type of the property's elements.\n     * @param audio_object_id The ID of the AudioObject to get the array from.\n     * @param address The address of the property.\n     * @param data_array The array to put the property data into.\n     * @return True if successful, or false if an error occurred.\n     */\n    template<typename T>\n    static bool get_property_array(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address, std::vector<T>& data_array);\n\n    /**\n     * Gets an array property.\n     * @tparam T The type of the property's elements.\n     * @param audio_object_id The ID of the AudioObject to query.\n     * @param address The address of the property.\n     * @return An array with the property's data, or an empty array if an error occurred.\n     */\n    template<typename T>\n    static std::vector<T> get_property_array(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address);\n\n    /**\n     * Retrieves the data size of the property for given address.\n     * @param audio_object_id The ID of the AudioObject to query.\n     * @param address The address of the property to lookup.\n     * @return The data size of the property, or 0 if the property does not exist or on any other error.\n     */\n    static UInt32 get_property_data_size(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address);\n\n    /**\n     * Tests whether the property for given address is settable.\n     * @param audio_object_id The ID of the AudioObject to query.\n     * @param address The address of the property to lookup.\n     * @return True if settable, or false if read-only or non existent.\n     */\n    static bool is_property_settable(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address);\n\n    /**\n     * Gets the property data for given address.\n     * @param audio_object_id The ID of the AudioObject to query.\n     * @param address The address of the property to get the data from.\n     * @param data_size The data size of data.\n     * @param data The memory of size data_size.\n     * @return The actual retrieved size of the data. It might be a lower number than the passed in data_size.\n     */\n    static UInt32 get_property_data(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address, UInt32 data_size, void* _Nonnull data);\n\n    /**\n     * Sets the data of property for given address.\n     * @param audio_object_id The ID of the AudioObject to set the property on.\n     * @param address The address of the property to set the data for.\n     * @param data_size The size of the data to set.\n     * @param data The data to set.\n     * @return True if successful, or false if an error occurred.\n     */\n    static bool set_property_data(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address, UInt32 data_size, const void* _Nonnull data);\n\n    /**\n     * Get a string property for given address.\n     * Note: please make sure that the property is of type CFStringRef, otherwise behaviour is undefined.\n     * @param audio_object_id The ID of the AudioObject to query.\n     * @param address The address of the property.\n     * @return A string containing the UTF8 representation of property at given address.\n     */\n    static std::string get_cfstring_property(AudioObjectID audio_object_id, const AudioObjectPropertyAddress& address);\n\nprotected:\n    /**\n     * Gets the data for property of type T for this AudioObject.\n     * @tparam T The type of the property (it's size must match the size of the property).\n     * @param address The address of the property.\n     * @return The property, or a default constructed value on error.\n     */\n    template<typename T>\n    [[nodiscard]] T get_property(const AudioObjectPropertyAddress& address) const\n    {\n        return get_property<T>(_audio_object_id, address);\n    }\n\n    /**\n     * Sets the data for property of type T for this AudioObject.\n     * @tparam T The type of the property (it's size must match the size of the property).\n     * @param address The address of the property.\n     * @param value The value to set.\n     * @return True if successful, or false if property could not be set.\n     */\n    template<typename T>\n    [[nodiscard]] bool set_property(const AudioObjectPropertyAddress& address, const T& value) const\n    {\n        return set_property(_audio_object_id, address, value);\n    }\n\n    /**\n     * Get a string property for given address for this AudioObject.\n     * Note: please make sure that the property is of type CFStringRef, otherwise behaviour is undefined.\n     * @param address The address of the property.\n     * @return A string containing the UTF8 representation of property at given address.\n     */\n    [[nodiscard]] std::string get_cfstring_property(const AudioObjectPropertyAddress& address) const;\n\n    /**\n     * Gets an array property for this AudioObject.\n     * @tparam T The type of the property's elements.\n     * @param address The address of the property.\n     * @param data_array The array to put the property data into.\n     * @return True if successful, or false if an error occurred.\n     */\n    template<typename T>\n    bool get_property_array(const AudioObjectPropertyAddress& address, std::vector<T>& data_array) const\n    {\n        return get_property_array(_audio_object_id, address, data_array);\n    }\n\n    /**\n     * Gets an array property for this AudioObject.\n     * @tparam T The type of the property's elements.\n     * @param address The address of the property.\n     * @return An array with the property's data, or an empty array if an error occurred.\n     */\n    template<typename T>\n    [[nodiscard]] std::vector<T> get_property_array(const AudioObjectPropertyAddress& address) const\n    {\n        return AudioObject::get_property_array<T>(_audio_object_id, address);\n    }\n\n    /**\n     * Tests if this AudioObject has property for given address for this AudioObject.\n     * @param address The address of the property to lookup.\n     * @return True if this object has the property, or false if not.\n     */\n    [[nodiscard]] bool has_property(const AudioObjectPropertyAddress& address) const;\n\n    /**\n     * Tests whether the property for given address is settable for this AudioObject.\n     * @param address The address of the property to lookup.\n     * @return True if settable, or false if read-only or non existent.\n     */\n    [[nodiscard]] bool is_property_settable(const AudioObjectPropertyAddress& address) const;\n\n    /**\n     * Retrieves the data size of the property for this AudioObject at given address.\n     * @param address The address of the property to lookup.\n     * @return The data size of the property, or 0 if the property does not exist or on any other error.\n     */\n    [[nodiscard]] UInt32 get_property_data_size(const AudioObjectPropertyAddress& address) const;\n\n    /**\n     * Gets the property data for given address.\n     * @param address The address of the property to get the data from.\n     * @param data_size The data size of data.\n     * @param data The memory of size data_size.\n     * @return The actual retrieved size of the data. It might be a lower number than the passed in data_size.\n     */\n    UInt32 get_property_data(const AudioObjectPropertyAddress& address, UInt32 data_size, void* _Nonnull data) const;\n\n    /**\n     * Sets the data of property for given address for this AudioObject.\n     * @param address The address of the property to set the data for.\n     * @param data_size The size of the data to set.\n     * @param data The data to set.\n     * @return True if successful, or false if an error occurred.\n     */\n    bool set_property_data(const AudioObjectPropertyAddress& address, UInt32 data_size, const void* _Nonnull data) const;\n\n    /**\n     * Adds a property listener for given address.\n     * @param address The address to install a property listener for.\n     * @return True if successful, or false if an error occurred.\n     * If a property listener for given address was already installed then the return value will be true.\n     */\n    bool add_property_listener(const AudioObjectPropertyAddress& address);\n\n    /**\n     * Called when a property (for which a listener is installed) changed.\n     * @param address The address of the property which changed.\n     */\n    virtual void property_changed([[maybe_unused]] const AudioObjectPropertyAddress& address) {}\n\nprivate:\n    static OSStatus _audio_object_property_listener_proc(AudioObjectID audio_object_id,\n                                                         UInt32 num_addresses,\n                                                         const AudioObjectPropertyAddress* _Nonnull address,\n                                                         void* __nullable client_data);\n\n    AudioObjectID _audio_object_id{0};\n    std::vector<AudioObjectPropertyAddress> _property_listeners;\n};\n\n} // namespace apple_coreaudio\n\n#endif\n"
  },
  {
    "path": "src/audio_frontends/apple_coreaudio/apple_coreaudio_system_object.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief C++ representation of the AudioObject as used in the CoreAudio apis.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_APPLE_COREAUDIO_SYSTEM_OBJECT_H\n#define SUSHI_APPLE_COREAUDIO_SYSTEM_OBJECT_H\n\n#include \"apple_coreaudio_device.h\"\n\nnamespace apple_coreaudio {\n/**\n * This class represents the Core Audio system object of which only one exists, system wide.\n */\nclass AudioSystemObject\n{\npublic:\n    static std::vector<AudioDevice> get_audio_devices()\n    {\n        auto device_ids = AudioObject::get_property_array<UInt32>(kAudioObjectSystemObject,\n                                                                  {kAudioHardwarePropertyDevices,\n                                                                   kAudioObjectPropertyScopeGlobal,\n                                                                   kAudioObjectPropertyElementMain});\n\n        std::vector<AudioDevice> audio_devices;\n        audio_devices.reserve(device_ids.size());\n\n        for (auto& id : device_ids)\n        {\n            audio_devices.emplace_back(id);\n        }\n\n        return audio_devices;\n    }\n\n    static AudioObjectID get_default_device_id(bool for_input)\n    {\n        AudioObjectPropertyAddress pa{for_input ? kAudioHardwarePropertyDefaultInputDevice\n                                                : kAudioHardwarePropertyDefaultOutputDevice,\n                                      kAudioObjectPropertyScopeGlobal,\n                                      kAudioObjectPropertyElementMain};\n        return AudioObject::get_property<AudioObjectID>(kAudioObjectSystemObject, pa);\n    };\n};\n\n} // namespace apple_coreaudio\n\n#endif // SUSHI_APPLE_COREAUDIO_SYSTEM_OBJECT_H\n"
  },
  {
    "path": "src/audio_frontends/apple_coreaudio/apple_coreaudio_utils.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include \"apple_coreaudio_utils.h\"\n\nnamespace apple_coreaudio {\n\nstd::string cf_string_to_std_string(const CFStringRef& cf_string_ref)\n{\n    if (cf_string_ref == nullptr)\n    {\n        return {};\n    }\n\n    // First try the cheap solution (no allocation). Not guaranteed to return anything.\n    // Note: for this particular case there is not really a benefit of using this function,\n    // because we're going to allocate a new string anyway, however in this case I prefer\n    // to use the 'best practice' here to educate myself properly in the future.\n    const auto* c_string = CFStringGetCStringPtr(cf_string_ref, kCFStringEncodingUTF8);\n    // The memory pointed to by c_string is owned by cf_string_ref.\n\n    if (c_string != nullptr)\n    {\n        return c_string;\n    }\n\n    // If the above didn't return anything we have to fall back and use CFStringGetCString.\n    CFIndex length = CFStringGetLength(cf_string_ref);\n    CFIndex max_size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; // Include room for \\0 termination\n\n    std::string output(max_size, 0);\n    if (!CFStringGetCString(cf_string_ref, output.data(), max_size, kCFStringEncodingUTF8))\n    {\n        return {};\n    }\n\n    // Remove all null-termination characters from output\n    output.erase(std::find(output.begin(), output.end(), '\\0'), output.end());\n\n    return output;\n}\n\nTimeConversions::TimeConversions()\n{\n    mach_timebase_info_data_t info{};\n    mach_timebase_info(&info);\n    _numerator = info.numer;\n    _denominator = info.denom;\n}\n\nuint64_t TimeConversions::host_time_to_nanos(uint64_t host_time_ticks) const\n{\n    return multiply_by_ratio(host_time_ticks, _numerator, _denominator);\n}\n\nuint64_t TimeConversions::nanos_to_host_time(uint64_t host_time_nanos) const\n{\n    return multiply_by_ratio(host_time_nanos, _denominator, _numerator); // NOLINT\n}\n\nuint64_t TimeConversions::multiply_by_ratio(uint64_t toMultiply, uint64_t numerator, uint64_t denominator)\n{\n#if defined(__SIZEOF_INT128__)\n    unsigned __int128 result = toMultiply;\n#else\n    long double result = toMultiply;\n#endif\n\n    if (numerator != denominator)\n    {\n        result *= numerator;\n        result /= denominator;\n    }\n\n    return (uint64_t) result;\n}\n\n} // namespace apple_coreaudio\n"
  },
  {
    "path": "src/audio_frontends/apple_coreaudio/apple_coreaudio_utils.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utilities for working with Apple's CoreAudio\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_APPLE_COREAUDIO_UTILS_H\n#define SUSHI_APPLE_COREAUDIO_UTILS_H\n\n#include <string>\n\n#include <mach/mach_time.h>\n#include <CoreAudio/AudioHardware.h>\n\n/**\n * Helper macro to log OSStatus errors in a consistent and convenient way.\n */\n#define CA_LOG_IF_ERROR(command)                                          \\\n    do                                                                    \\\n    {                                                                     \\\n        OSStatus result = command;                                        \\\n        if (result != kAudioHardwareNoError)                              \\\n        {                                                                 \\\n            ELKLOG_LOG_ERROR(\"{} returned error : {}\", #command, result); \\\n        }                                                                 \\\n    } while (false)\n\n/**\n * Helper macro to return with a value when the OSStatus indicates an error.\n */\n#define CA_RETURN_IF_ERROR(command, return_value)                         \\\n    do                                                                    \\\n    {                                                                     \\\n        OSStatus result = command;                                        \\\n        if (result != kAudioHardwareNoError)                              \\\n        {                                                                 \\\n            ELKLOG_LOG_ERROR(\"{} returned error : {}\", #command, result); \\\n            return return_value;                                          \\\n        }                                                                 \\\n    } while (false)\n\n/**\n * Implements the comparison operator for AudioObjectPropertyAddress.\n * @param lhs Left hand side\n * @param rhs Right hand side\n * @return True if equal, or false if not.\n */\ninline bool operator==(const AudioObjectPropertyAddress& lhs, const AudioObjectPropertyAddress& rhs)\n{\n    return lhs.mElement == rhs.mElement && lhs.mScope == rhs.mScope && lhs.mSelector && rhs.mSelector;\n}\n\n/**\n * Implements inequality operator for AudioObjectPropertyAddress.\n * @param lhs Left hand side.\n * @param rhs Right hand side.\n * @return True if not equal, or false if it is.\n */\ninline bool operator!=(const AudioObjectPropertyAddress& lhs, const AudioObjectPropertyAddress& rhs)\n{\n    return !(rhs == lhs);\n}\n\nnamespace apple_coreaudio {\n\n/**\n * Converts given CFString to an std::string.\n * @param cf_string_ref The CFString to convert.\n * @return A standard string with the contents of given CFString, encoded as UTF8.\n */\nstd::string cf_string_to_std_string(const CFStringRef& cf_string_ref);\n\n/**\n * Little struct which holds host time information and provides facilities to convert from host to real time and vice versa.\n */\nstruct TimeConversions {\npublic:\n    TimeConversions();\n\n    /**\n     * Converts host time to nanoseconds.\n     * @param host_time_ticks The host time in ticks.\n     * @return The host time in nanoseconds.\n     */\n    [[nodiscard]] uint64_t host_time_to_nanos(uint64_t host_time_ticks) const;\n\n    /**\n     * Converts nanoseconds to host time.\n     * @param host_time_nanos Time in nanoseconds.\n     * @return The host time in ticks.\n     */\n    [[nodiscard]] uint64_t nanos_to_host_time(uint64_t host_time_nanos) const;\n\nprivate:\n    // Adapted from CAHostTimeBase.h in the Core Audio Utility Classes\n    static uint64_t multiply_by_ratio(uint64_t toMultiply, uint64_t numerator, uint64_t denominator);\n\n    uint64_t _numerator = 0, _denominator = 0;\n};\n\n} // namespace apple_coreaudio\n\n#endif // SUSHI_APPLE_COREAUDIO_UTILS_H\n"
  },
  {
    "path": "src/audio_frontends/apple_coreaudio_frontend.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Realtime audio frontend for Apple CoreAudio\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifdef SUSHI_BUILD_WITH_APPLE_COREAUDIO\n\n#include \"elklog/static_logger.h\"\n\n#include \"apple_coreaudio_frontend.h\"\n#include \"apple_coreaudio/apple_coreaudio_system_object.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"AppleCoreAudio\");\n\nnamespace sushi::internal::audio_frontend {\n\nstd::optional<std::string> get_coreaudio_output_device_name(std::optional<std::string> coreaudio_output_device_uid)\n{\n    auto audio_devices = apple_coreaudio::AudioSystemObject::get_audio_devices();\n\n    if (audio_devices.empty())\n    {\n        ELKLOG_LOG_ERROR(\"No Apple CoreAudio devices found\");\n        return std::nullopt;\n    }\n\n    std::string uid;\n    if (coreaudio_output_device_uid.has_value())\n    {\n        uid = coreaudio_output_device_uid.value();\n    }\n    else\n    {\n        auto default_audio_output_device_id = apple_coreaudio::AudioSystemObject::get_default_device_id(false); // false to get the output device id\n        apple_coreaudio::AudioDevice default_audio_output_device(default_audio_output_device_id);\n        uid = default_audio_output_device.uid();\n    }\n\n    for (auto& device : audio_devices)\n    {\n        if (device.uid() == uid)\n        {\n            return device.name();\n        }\n    }\n\n    if (coreaudio_output_device_uid.has_value())\n    {\n        ELKLOG_LOG_ERROR(\"Could not retrieve device name for coreaudio device with uid: {}\",\n                        coreaudio_output_device_uid.value());\n    }\n    else\n    {\n        ELKLOG_LOG_ERROR(\"Could not retrieve device name for default coreaudio device, uid: {}\", uid);\n    }\n\n    return std::nullopt;\n}\n\nAppleCoreAudioFrontend::AppleCoreAudioFrontend(engine::BaseEngine* engine) : BaseAudioFrontend(engine) {}\n\nAudioFrontendStatus AppleCoreAudioFrontend::init(BaseAudioFrontendConfiguration* config)\n{\n    if (config == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Invalid config given\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    auto coreaudio_config = static_cast<AppleCoreAudioFrontendConfiguration*>(config); // NOLINT: Clang-Tidy: Do not use static_cast to downcast from a base to a derived class; use dynamic_cast instead\n\n    auto ret_code = BaseAudioFrontend::init(config);\n    if (ret_code != AudioFrontendStatus::OK)\n    {\n        return ret_code;\n    }\n\n    std::string input_device_uid;\n    std::string output_device_uid;\n\n    if (coreaudio_config->input_device_uid->empty())\n    {\n        auto input_id = apple_coreaudio::AudioSystemObject::get_default_device_id(true);\n        apple_coreaudio::AudioDevice default_audio_input_device(input_id);\n        input_device_uid = default_audio_input_device.uid();\n        ELKLOG_LOG_INFO(\"Input device not specified, using default: {}\", input_device_uid);\n    }\n    else\n    {\n        input_device_uid = coreaudio_config->input_device_uid.value();\n    }\n    if (coreaudio_config->output_device_uid->empty())\n    {\n        auto output_id = apple_coreaudio::AudioSystemObject::get_default_device_id(false);\n        apple_coreaudio::AudioDevice default_audio_output_device(output_id);\n        output_device_uid = default_audio_output_device.uid();\n        ELKLOG_LOG_INFO(\"Output device not specified, using default: {}\", output_device_uid);\n    }\n    else\n    {\n        output_device_uid = coreaudio_config->output_device_uid.value();\n    }\n\n    auto devices = apple_coreaudio::AudioSystemObject::get_audio_devices();\n\n    if (input_device_uid == output_device_uid)\n    {\n        // Input device is same as output device. We're going to open a single device.\n\n        AudioObjectID audio_device_id = 0;\n        if (auto* audio_device = device_for_uid(devices, output_device_uid))\n        {\n            audio_device_id = audio_device->get_audio_object_id();\n        }\n\n        if (audio_device_id == 0)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to open audio device for specified UID\");\n            return AudioFrontendStatus::AUDIO_HW_ERROR;\n        }\n\n        _audio_device = std::make_unique<apple_coreaudio::AudioDevice>(audio_device_id);\n    }\n    else\n    {\n        // Input device is not the same as the output device. Let's create an aggregate device.\n\n        auto* input_audio_device = device_for_uid(devices, input_device_uid);\n        auto* output_audio_device = device_for_uid(devices, output_device_uid);\n\n        if (input_audio_device == nullptr || output_audio_device == nullptr)\n        {\n            ELKLOG_LOG_ERROR(\"Device not found\");\n            return AudioFrontendStatus::AUDIO_HW_ERROR;\n        }\n\n        auto aggregate_device = apple_coreaudio::AudioDevice::create_aggregate_device(*input_audio_device, *output_audio_device);\n\n        if (!aggregate_device)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to create aggregate device\");\n            return AudioFrontendStatus::AUDIO_HW_ERROR;\n        }\n\n        _audio_device = std::move(aggregate_device);\n    }\n\n    auto channel_conf_result = configure_audio_channels(coreaudio_config);\n    if (channel_conf_result != AudioFrontendStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to configure audio channels\");\n        return channel_conf_result;\n    }\n\n    double sample_rate = _engine->sample_rate();\n\n    if (!_audio_device->is_valid())\n    {\n        ELKLOG_LOG_ERROR(\"Invalid output device\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    if (!_audio_device->set_buffer_frame_size(AUDIO_CHUNK_SIZE))\n    {\n        ELKLOG_LOG_ERROR(\"Failed to set buffer size to {} for output device \\\"{}\\\"\", AUDIO_CHUNK_SIZE, _audio_device->name());\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    if (!_audio_device->set_nominal_sample_rate(sample_rate))\n    {\n        ELKLOG_LOG_ERROR(\"Failed to set sample rate to {} for output device \\\"{}\\\"\", sample_rate, _audio_device->name());\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    _set_engine_sample_rate(static_cast<float>(sample_rate));\n\n    UInt32 input_latency = _audio_device->device_latency(true) + _audio_device->selected_stream_latency(true);\n    UInt32 output_latency = _audio_device->device_latency(false) + _audio_device->selected_stream_latency(false);\n\n    auto useconds = std::chrono::microseconds(output_latency * 1'000'000 / static_cast<UInt32>(sample_rate));\n    _engine->set_output_latency(useconds);\n\n    ELKLOG_LOG_INFO(\"Stream started, using input latency {}ms and output latency {}ms\",\n                   input_latency * 1'000 / static_cast<UInt32>(sample_rate),\n                   output_latency * 1'000 / static_cast<UInt32>(sample_rate));\n\n    (void) input_latency; // Ignore variable-not-used warning when compiling without Sushi logging (ie. UnitTests)\n\n    return AudioFrontendStatus::OK;\n}\n\nvoid AppleCoreAudioFrontend::cleanup()\n{\n    if (_engine != nullptr)\n    {\n        _engine->enable_realtime(false);\n    }\n\n    if (!stop_io())\n    {\n        ELKLOG_LOG_ERROR(\"Failed to stop audio device(s)\");\n    }\n}\n\nvoid AppleCoreAudioFrontend::run()\n{\n    _engine->enable_realtime(true);\n\n    if (!start_io())\n    {\n        ELKLOG_LOG_ERROR(\"Failed to start audio device(s)\");\n    }\n}\n\nAudioFrontendStatus AppleCoreAudioFrontend::configure_audio_channels(const AppleCoreAudioFrontendConfiguration* config)\n{\n    if (config->cv_inputs > 0 || config->cv_outputs > 0)\n    {\n        ELKLOG_LOG_ERROR(\"CV ins and outs not supported and must be set to 0\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    _device_num_input_channels = _audio_device->num_channels(true);\n    _device_num_output_channels = _audio_device->num_channels(false);\n\n    if (_device_num_input_channels < 0 || _device_num_output_channels < 0)\n    {\n        ELKLOG_LOG_ERROR(\"Invalid number of channels ({}/{})\", _device_num_input_channels, _device_num_output_channels);\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    auto num_input_channels = std::min(_device_num_input_channels, MAX_FRONTEND_CHANNELS);\n    auto num_output_channels = std::min(_device_num_output_channels, MAX_FRONTEND_CHANNELS);\n\n    _in_buffer = ChunkSampleBuffer(num_input_channels);\n    _out_buffer = ChunkSampleBuffer(num_output_channels);\n\n    _engine->set_audio_channels(num_input_channels, num_output_channels);\n\n    auto status = _engine->set_cv_input_channels(config->cv_inputs);\n    if (status != engine::EngineReturnStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to setup CV input channels\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    status = _engine->set_cv_output_channels(config->cv_outputs);\n    if (status != engine::EngineReturnStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to setup CV output channels\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    ELKLOG_LOG_DEBUG(\"Setting up CoreAudio with {} inputs {} outputs\", num_input_channels, num_output_channels);\n\n    if (num_input_channels > 0)\n    {\n        ELKLOG_LOG_INFO(\"Connected input channels to \\\"{}\\\"\", _audio_device->name(apple_coreaudio::AudioDevice::Scope::INPUT));\n        ELKLOG_LOG_INFO(\"Input device has {} available channels\", _device_num_input_channels);\n    }\n    else\n    {\n        ELKLOG_LOG_INFO(\"No input channels found, not connecting to input device\");\n    }\n\n    if (num_output_channels > 0)\n    {\n        ELKLOG_LOG_INFO(\"Connected output channels to \\\"{}\\\"\", _audio_device->name(apple_coreaudio::AudioDevice::Scope::OUTPUT));\n        ELKLOG_LOG_INFO(\"Output device has {} available channels\", _device_num_output_channels);\n    }\n    else\n    {\n        ELKLOG_LOG_INFO(\"No output channels found, not connecting to output device\");\n    }\n\n    return AudioFrontendStatus::OK;\n}\n\nbool AppleCoreAudioFrontend::start_io()\n{\n    if (!_audio_device->start_io(this))\n    {\n        return false;\n    }\n\n    return true;\n}\n\nbool AppleCoreAudioFrontend::stop_io()\n{\n    bool result = true;\n\n    if (_audio_device->is_valid() && !_audio_device->stop_io())\n    {\n        result = false;\n    }\n\n    return result;\n}\n\nvoid AppleCoreAudioFrontend::audio_callback(const float* input_data, int num_input_channels, float* output_data, int num_output_channels, int num_samples, uint64_t host_input_time)\n{\n    _out_buffer.clear();\n    assert(num_samples == AUDIO_CHUNK_SIZE);\n    std::chrono::microseconds current_time(_time_conversions.host_time_to_nanos(host_input_time) / 1000);\n    _handle_resume(current_time, num_samples);\n\n    if (_pause_manager.should_process())\n    {\n        _copy_interleaved_audio_to_input_buffer(input_data, num_input_channels);\n        _engine->process_chunk(&_in_buffer, &_out_buffer, &_in_controls, &_out_controls, current_time, _processed_sample_count);\n\n        if (_pause_manager.should_ramp())\n        {\n            _pause_manager.ramp_output(_out_buffer);\n        }\n    }\n\n    _handle_pause(current_time);\n    _copy_output_buffer_to_interleaved_buffer(output_data, num_output_channels);\n\n    _processed_sample_count += num_samples;\n}\n\nvoid AppleCoreAudioFrontend::sample_rate_changed(double new_sample_rate)\n{\n    ELKLOG_LOG_WARNING(\"Audio device changed sample rate to: {}\", new_sample_rate);\n\n#ifdef EXIT_SUSHI_WHEN_AUDIO_DEVICE_CHANGES_TO_INCOMPATIBLE_SAMPLE_RATE\n    // The next piece of code is ugly as **** but prevents a lot of engineering to get to what we need:\n    // notifying the user of Elk LIVE Desktop that the sample rate of their device has changed.\n    // We do that by exiting the application (from a background thread) with a specific return value which\n    // gets interpreted by Elk LIVE Desktop as the reason being the sample rate change.\n    //\n    // Doing this the proper way would look something like this:\n    //   - Install some sort of event loop on the main thread\n    //   - Allow other threads to schedule work on this event loop\n    //   - Allow other threads to signal the event loop to exit (which results in a clean application exit)\n    //\n    // One way of creating a simple event loop would be to use a concurrent queue like this:\n    //\n    // moodycamel::BlockingConcurrentQueue<std::function<void()>> q;\n    //\n    // std::function<bool()> work; // Return value: true to continue the main event loop or false to exit\n    //\n    // for (;;)\n    // {\n    //     if (!q.wait_dequeue_timed(work, std::chrono::milliseconds(500))\n    //     {\n    //         continue; // Nothing dequeued.\n    //     }\n    //\n    //     if (!work)\n    //     {\n    //         ELKLOG_LOG_ERROR(\"Received nullptr function\");\n    //         break;\n    //     }\n    //\n    //     if (!work()) // A return value of false means exit the event loop.\n    //     {\n    //         break;\n    //     }\n    // }\n\n    // Assuming the sample rate doesn't change during audio processing, otherwise we would have a race condition because this method gets called from a background thread.\n    // Since the sample rate doesn't change during processing, the next line will always read the correct value which is acceptable in this exceptional case.\n    if (std::abs(new_sample_rate - _engine->sample_rate()) > 1.0)\n    {\n        ELKLOG_LOG_WARNING(\"Exiting Sushi in response to incompatible external sample rate change (return value: {})\", EXIT_RETURN_VALUE_ON_INCOMPATIBLE_SAMPLE_RATE_CHANGE);\n        exit(EXIT_RETURN_VALUE_ON_INCOMPATIBLE_SAMPLE_RATE_CHANGE);\n    }\n#endif\n}\n\nvoid AppleCoreAudioFrontend::_copy_interleaved_audio_to_input_buffer(const float* input, int num_channels)\n{\n    for (int ch = 0; ch < std::min(num_channels, _in_buffer.channel_count()); ch++)\n    {\n        float* in_dst = _in_buffer.channel(ch);\n        for (size_t s = 0; s < AUDIO_CHUNK_SIZE; s++)\n        {\n            in_dst[s] = input[s * num_channels + ch];\n        }\n    }\n}\n\nvoid AppleCoreAudioFrontend::_copy_output_buffer_to_interleaved_buffer(float* output, int num_channels)\n{\n    for (int ch = 0; ch < std::min(num_channels, _out_buffer.channel_count()); ch++)\n    {\n        const float* out_src = _out_buffer.channel(ch);\n        for (size_t s = 0; s < AUDIO_CHUNK_SIZE; s++)\n        {\n            output[s * num_channels + ch] = out_src[s];\n        }\n    }\n}\n\n} // namespace sushi::internal::audio_frontend\n\n#endif // SUSHI_BUILD_WITH_APPLE_COREAUDIO\n\n#ifndef SUSHI_BUILD_WITH_APPLE_COREAUDIO\n#include \"elklog/static_logger.h\"\n\n#include \"apple_coreaudio_frontend.h\"\n\nELKLOG_GET_LOGGER;\n\nnamespace sushi::internal::audio_frontend {\n\nsushi::internal::audio_frontend::AudioFrontendStatus sushi::internal::audio_frontend::AppleCoreAudioFrontend::init(\n        [[maybe_unused]] sushi::internal::audio_frontend::BaseAudioFrontendConfiguration* config)\n{\n    // The log print needs to be in a cpp file for initialisation order reasons\n    ELKLOG_LOG_ERROR(\"Sushi was not built with CoreAudio support!\");\n    return AudioFrontendStatus::AUDIO_HW_ERROR;\n}\n\n} // namespace sushi::audio_frontend\n\n#endif // SUSHI_BUILD_WITH_APPLE_COREAUDIO\n"
  },
  {
    "path": "src/audio_frontends/apple_coreaudio_frontend.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Realtime audio frontend for Apple CoreAudio\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_APPLE_COREAUDIO_FRONTEND_H\n#define SUSHI_APPLE_COREAUDIO_FRONTEND_H\n\n#ifdef SUSHI_BUILD_WITH_APPLE_COREAUDIO\n\n#include <memory>\n\n#include \"base_audio_frontend.h\"\n#include \"apple_coreaudio/apple_coreaudio_object.h\"\n#include \"apple_coreaudio/apple_coreaudio_device.h\"\n\n// See AppleCoreAudioFrontend::sample_rate_changed()\n#define EXIT_SUSHI_WHEN_AUDIO_DEVICE_CHANGES_TO_INCOMPATIBLE_SAMPLE_RATE\n\nnamespace sushi::internal::audio_frontend {\n\n[[nodiscard]] std::optional<std::string> get_coreaudio_output_device_name(std::optional<std::string> coreaudio_output_device_uid);\n\nstruct AppleCoreAudioFrontendConfiguration : public BaseAudioFrontendConfiguration {\n    AppleCoreAudioFrontendConfiguration(std::optional<std::string> input_device_uid,\n                                        std::optional<std::string> output_device_uid,\n                                        int cv_inputs,\n                                        int cv_outputs) : BaseAudioFrontendConfiguration(cv_inputs, cv_outputs),\n                                                          input_device_uid(std::move(input_device_uid)),\n                                                          output_device_uid(std::move(output_device_uid))\n    {}\n\n    ~AppleCoreAudioFrontendConfiguration() override = default;\n\n    std::optional<std::string> input_device_uid;\n    std::optional<std::string> output_device_uid;\n};\n\nclass AppleCoreAudioFrontend : public BaseAudioFrontend, private apple_coreaudio::AudioDevice::AudioCallback\n{\npublic:\n    explicit AppleCoreAudioFrontend(engine::BaseEngine* engine);\n    ~AppleCoreAudioFrontend() override = default;\n\n    AudioFrontendStatus init(BaseAudioFrontendConfiguration* config) override;\n    void cleanup() override;\n    void run() override;\n\n    AudioFrontendStatus configure_audio_channels(const AppleCoreAudioFrontendConfiguration* config);\n\n    bool start_io();\n    bool stop_io();\n\nprivate:\n    std::unique_ptr<apple_coreaudio::AudioDevice> _audio_device;\n    apple_coreaudio::TimeConversions _time_conversions;\n    int _device_num_input_channels{0};\n    int _device_num_output_channels{0};\n    ChunkSampleBuffer _in_buffer{0};\n    ChunkSampleBuffer _out_buffer{0};\n    engine::ControlBuffer _in_controls;\n    engine::ControlBuffer _out_controls;\n    int64_t _processed_sample_count{0};\n\n    // See the body of sample_rate_changed for an explanation for this variable.\n    // Note: Elk LIVE Desktop depends on this specific return value.\n    static constexpr int EXIT_RETURN_VALUE_ON_INCOMPATIBLE_SAMPLE_RATE_CHANGE = 55;\n\n    void _copy_interleaved_audio_to_input_buffer(const float* input, int num_channels);\n    void _copy_output_buffer_to_interleaved_buffer(float* output, int num_channels);\n\n    // apple_coreaudio::AudioDevice::AudioCallback overrides\n    void audio_callback(const float* input_data, int num_input_channels, float* output_data, int num_output_channels, int num_samples, uint64_t host_input_time) override;\n\n    void sample_rate_changed(double new_sample_rate) override;\n};\n\n} // namespace sushi::internal::audio_frontend\n\n#endif // SUSHI_BUILD_WITH_APPLE_COREAUDIO\n\n#ifndef SUSHI_BUILD_WITH_APPLE_COREAUDIO\n/* If Apple CoreAudio is disabled in the build config, the AppleCoreAudio frontend is replaced with\n   this dummy frontend whose only purpose is to assert if you try to use it */\n\n#include \"base_audio_frontend.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nstruct AppleCoreAudioFrontendConfiguration : public BaseAudioFrontendConfiguration {\n    AppleCoreAudioFrontendConfiguration(const std::optional<std::string>&,\n                                        const std::optional<std::string>&,\n                                        int,\n                                        int) : BaseAudioFrontendConfiguration(0, 0) {}\n};\n\nclass AppleCoreAudioFrontend : public BaseAudioFrontend\n{\npublic:\n    explicit AppleCoreAudioFrontend(engine::BaseEngine* engine) : BaseAudioFrontend(engine) {};\n    AudioFrontendStatus init(BaseAudioFrontendConfiguration* config) override;\n\n    void cleanup() override{};\n    void run() override{};\n};\n\n} // namespace sushi::internal::audio_frontend\n\n#endif // SUSHI_BUILD_WITH_APPLE_COREAUDIO\n\n#endif // SUSHI_APPLE_COREAUDIO_FRONTEND_H\n"
  },
  {
    "path": "src/audio_frontends/audio_frontend_internals.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Common implementation details shared between audio frontends\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n#ifndef SUSHI_AUDIO_FRONTEND_INTERNALS_H\n#define SUSHI_AUDIO_FRONTEND_INTERNALS_H\n\n#ifdef __x86_64__\n#include <xmmintrin.h>\n#endif\n\nnamespace sushi::internal::audio_frontend {\n\n/* These are calculated theoretical correction factors from the Sika board and\n * provide a mapping that works fine for parameters and reasonable for pitch.\n * Pitch mapping that need extra precision will need separate calibration for\n * each cv port.\n * The CV_IN_CORR also accounts for the fact that the cv inputs are inverted\n * in the Sika board. Could eventually be fixed in the driver */\n\nconstexpr float CV_OUT_CORR = 0.987f;\nconstexpr float CV_IN_CORR = -1.449f;\n\n/**\n * @brief Sets the FTZ (flush denormals to zero) and DAC (denormals are zero) flags\n *        in the cpu to avoid performance hits of denormals in the audio thread. This\n *        is only needed for x86 based machines as ARM machines have it disabled by\n *        default if vectorization is enabled.\n */\ninline void set_flush_denormals_to_zero()\n{\n    #ifdef __x86_64__\n    _mm_setcsr(0x9FC0);\n    #endif\n}\n\n/**\n * @return Maps a sample from an audio input [-1, 1] range to cv range [0, 1]\n * @param audio An audio sample\n * @return a float remapped to a [0, 1] range.\n */\ninline float map_audio_to_cv(float audio)\n{\n    return (audio + 1.0f) * 0.5f;\n}\n\n/**\n * @return Maps a sample from a cv input [0, 1] range to audio range [-1, 1]\n * @param cv A cv value\n * @return a float remapped to a [-1, 1] range.\n */\ninline float map_cv_to_audio(float cv)\n{\n    return cv * 2.0f - 1.0f;\n}\n\n/**\n * @brief Helper function to do ramping of cv outputs that are updated once per\n *        audio chunk.\n * @param output A float array of size AUDIO_CHUNK_SIZE where the smoothed data will\n *               be copied to.\n * @param current_value The current value of the smoother\n * @param target_value The target value for the smoother\n * @return The new current value\n */\ninline float ramp_cv_output(float* output, float current_value, float target_value)\n{\n    float inc = (target_value - current_value) / (AUDIO_CHUNK_SIZE - 1);\n    for (int i = 0 ; i < AUDIO_CHUNK_SIZE; ++i)\n    {\n        output[i] = current_value + inc * i;\n    }\n    return target_value;\n}\n\n} // end namespace sushi::internal::audio_frontend\n\n#endif // SUSHI_AUDIO_FRONTEND_INTERNALS_H\n"
  },
  {
    "path": "src/audio_frontends/base_audio_frontend.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Audio frontend base classes implementations\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"base_audio_frontend.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"audio_frontend\");\n\nnamespace sushi::internal::audio_frontend {\n\nconstexpr float XRUN_LIMIT_FACTOR = 1.8f;\n\nAudioFrontendStatus BaseAudioFrontend::init(BaseAudioFrontendConfiguration* config)\n{\n    _config = config;\n    try\n    {\n        _pause_notify = twine::RtConditionVariable::create_rt_condition_variable();\n    }\n    catch ([[maybe_unused]] const std::exception& e)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to instantiate RtConditionVariable ({})\", e.what());\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    return AudioFrontendStatus::OK;\n}\n\nvoid BaseAudioFrontend::pause(bool paused)\n{\n    /* This default implementation is for realtime frontends that must remember to call\n     * _handle_resume() and _handle_pause() in the audio callback */\n    assert(twine::is_current_thread_realtime() == false);\n    bool running = !_pause_manager.bypassed();\n    _pause_manager.set_bypass(paused, _engine->sample_rate());\n\n    // If pausing, return when engine has ramped down.\n    if (paused && running)\n    {\n        _pause_notified = false;\n        _pause_notify->wait();\n        _engine->enable_realtime(false);\n        _resume_notified = false;\n    }\n    else\n    {\n        if (!paused and !running)\n        {\n            _engine->enable_realtime(true);\n        }\n    }\n}\n\nstd::pair<bool, Time> BaseAudioFrontend::_test_for_xruns(Time current_time, int current_samples)\n{\n    auto delta_time = current_time - _last_process_time;\n    bool first = _last_process_time == Time::zero();\n    auto limit = Time(static_cast<int>(current_samples * _inv_sample_rate * XRUN_LIMIT_FACTOR * Time(std::chrono::seconds(1)).count()));\n    _last_process_time = current_time;\n\n    if (!first && (delta_time != Time::zero()) && (std::abs(delta_time.count()) > limit.count()))\n    {\n        return {true, delta_time};\n    }\n    return {false, Time(0)};\n}\n\nvoid BaseAudioFrontend::_handle_resume(Time current_time, int current_samples)\n{\n    if (!_resume_notified && _pause_manager.should_process())\n    {\n        _resume_notified = true;\n        _engine->notify_interrupted_audio(current_time - _pause_start);\n    }\n    else\n    {\n        auto [xrun, delta_time] = _test_for_xruns(current_time, current_samples);\n        if (xrun)\n        {\n            _engine->notify_interrupted_audio(delta_time);\n        }\n    }\n}\n\nvoid BaseAudioFrontend::_handle_pause(Time current_time)\n{\n    if (_pause_notified == false && _pause_manager.should_process() == false)\n    {\n        _pause_notify->notify();\n        _pause_notified = true;\n        _pause_start = current_time;\n    }\n}\n\nvoid BaseAudioFrontend::_set_engine_sample_rate(float sample_rate)\n{\n    if (_engine->sample_rate() != sample_rate)\n    {\n        _engine->set_sample_rate(sample_rate);\n    }\n    _sample_rate = sample_rate;\n    _inv_sample_rate = 1.0f / sample_rate;\n}\n\n} // end namespace sushi::internal::audio_frontend"
  },
  {
    "path": "src/audio_frontends/base_audio_frontend.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Base classes for audio frontends\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n#ifndef SUSHI_BASE_AUDIO_FRONTEND_H\n#define SUSHI_BASE_AUDIO_FRONTEND_H\n\n#include \"engine/base_engine.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nconstexpr int MAX_FRONTEND_CHANNELS = 8;\n\n/**\n * @brief Error codes returned from init()\n */\nenum class AudioFrontendStatus\n{\n    OK,\n    INVALID_N_CHANNELS,\n    INVALID_INPUT_FILE,\n    INVALID_OUTPUT_FILE,\n    INVALID_SEQUENCER_DATA,\n    INVALID_CHUNK_SIZE,\n    AUDIO_HW_ERROR\n};\n\n/**\n * @brief Dummy base class to hold frontend configurations\n */\nstruct BaseAudioFrontendConfiguration\n{\n    BaseAudioFrontendConfiguration(int cv_inputs, int cv_outputs) : cv_inputs{cv_inputs},\n                                                                    cv_outputs{cv_outputs} {}\n\n    virtual ~BaseAudioFrontendConfiguration() = default;\n    int cv_inputs;\n    int cv_outputs;\n};\n\n/**\n * @brief Base class for Engine Frontends.\n */\nclass BaseAudioFrontend\n{\npublic:\n    explicit BaseAudioFrontend(engine::BaseEngine* engine) : _engine(engine) {}\n\n    virtual ~BaseAudioFrontend() = default;\n\n    /**\n     * @brief Initialize frontend with the given configuration.\n     *        If anything can go wrong during initialization, partially allocated\n     *        resources should be freed by calling cleanup().\n     *\n     * @param config Should be an object of the proper derived configuration class.\n     * @return AudioFrontendInitStatus::OK in case of success,\n     *         or different error code otherwise.\n     */\n    virtual AudioFrontendStatus init(BaseAudioFrontendConfiguration* config);\n\n    /**\n     * @brief Free resources allocated during init. stops the frontend if currently running.\n     */\n    virtual void cleanup() = 0;\n\n    /**\n     * @brief Run engine main loop.\n     */\n    virtual void run() = 0;\n\n    /**\n     * @brief Pause a running frontend. If paused, any threads set up are still running and audio\n     *        data consumed, but the audio engine is not called and all audio outputs are silenced.\n     *        When toggling pause, the audio will be quickly ramped down and the function will block\n     *        until the change has taken effect.\n     * @param paused If true enables pause, of false disables pause and calls the audio engine again\n     */\n     virtual void pause(bool paused);\n\nprotected:\n    /**\n     * @brief Call before calling engine->process_chunk for default handling of resume and xrun detection\n     * @param current_time Audio timestamp of the current audio callback\n     * @param current_sample Number of samples in the current audio callback\n     */\n    void _handle_resume(Time current_time, int current_samples);\n\n    /**\n     * @brief Call after calling engine->process_chunk for default handling of externally triggered pause\n     * @param current_time Audio timestamp of the current audio callback\n     */\n    void _handle_pause(Time current_time);\n\n    /**\n     * @brief Set the samplerate used for internal calculations and set the engine samplerate\n     * @param sample_rate The new sample rate in Hz\n     */\n    virtual void _set_engine_sample_rate(float sample_rate);\n\n    std::pair<bool, Time> _test_for_xruns(Time current_time, int current_samples);\n\n    BaseAudioFrontendConfiguration* _config {nullptr};\n    engine::BaseEngine* _engine {nullptr};\n\n    BypassManager _pause_manager;\n    std::unique_ptr<twine::RtConditionVariable> _pause_notify;\n    std::atomic_bool _pause_notified {false};\n    std::atomic_bool _resume_notified {true};\n    Time _pause_start;\n\n    Time _last_process_time{Time(0)};\n    float _sample_rate;\n    float _inv_sample_rate;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_BASE_AUDIO_FRONTEND_H\n"
  },
  {
    "path": "src/audio_frontends/coreaudio_devices_dump.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utility functions for dumping CoreAudio devices info\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n#include \"rapidjson/rapidjson.h\"\n\n#include \"sushi/coreaudio_devices_dump.h\"\n\n#ifdef SUSHI_BUILD_WITH_APPLE_COREAUDIO\n\n#include \"apple_coreaudio_frontend.h\"\n\n#include \"audio_frontends/apple_coreaudio/apple_coreaudio_system_object.h\"\n\nnamespace sushi {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"coreaudio\");\n\nrapidjson::Document generate_coreaudio_devices_info_document()\n{\n    internal::audio_frontend::AppleCoreAudioFrontend frontend{nullptr};\n\n    rapidjson::Document document;\n    document.SetObject();\n    rapidjson::Document::AllocatorType& allocator = document.GetAllocator();\n\n    auto audio_devices = apple_coreaudio::AudioSystemObject::get_audio_devices();\n    if (audio_devices.empty())\n    {\n        ELKLOG_LOG_ERROR(\"No Apple CoreAudio devices found\");\n        return document;\n    }\n\n    rapidjson::Value ca_devices(rapidjson::kObjectType);\n    rapidjson::Value devices(rapidjson::kArrayType);\n    for (auto& device : audio_devices)\n    {\n        rapidjson::Value device_obj(rapidjson::kObjectType);\n        device_obj.AddMember(rapidjson::Value(\"name\", allocator).Move(),\n                             rapidjson::Value(device.name().c_str(), allocator).Move(), allocator);\n        device_obj.AddMember(rapidjson::Value(\"uid\", allocator).Move(),\n                             rapidjson::Value(device.uid().c_str(), allocator).Move(), allocator);\n        device_obj.AddMember(rapidjson::Value(\"inputs\", allocator).Move(),\n                             rapidjson::Value(device.num_channels(true)).Move(), allocator);\n        device_obj.AddMember(rapidjson::Value(\"outputs\", allocator).Move(),\n                             rapidjson::Value(device.num_channels(false)).Move(), allocator);\n\n        // Add available sample rates as array\n        rapidjson::Value sample_rates(rapidjson::kArrayType);\n        for (auto& rate : device.available_nominal_sample_rates())\n        {\n            sample_rates.PushBack(static_cast<uint64_t>(rate), allocator);\n        }\n\n        device_obj.AddMember(rapidjson::Value(\"available_sample_rates\", allocator).Move(), sample_rates, allocator);\n\n        // Add available buffer sizes as object with min and max values\n        rapidjson::Value buffer_frame_size_range(rapidjson::kObjectType);\n\n        auto buffer_sizes = device.available_buffer_sizes();\n        buffer_frame_size_range.AddMember(rapidjson::Value(\"min\", allocator).Move(), buffer_sizes.mMinimum, allocator);\n        buffer_frame_size_range.AddMember(rapidjson::Value(\"max\", allocator).Move(), buffer_sizes.mMaximum, allocator);\n\n        device_obj.AddMember(rapidjson::Value(\"buffer_frame_size_range\", allocator).Move(), buffer_frame_size_range.Move(), allocator);\n\n        devices.PushBack(device_obj.Move(), allocator);\n    }\n    ca_devices.AddMember(rapidjson::Value(\"devices\", allocator).Move(), devices.Move(), allocator);\n\n    auto add_default_device_index = [&audio_devices, &ca_devices, &allocator](bool for_input) {\n        auto default_audio_device_object_id = apple_coreaudio::AudioSystemObject::get_default_device_id(for_input);\n\n        for (auto it = audio_devices.begin(); it != audio_devices.end(); it++)\n        {\n            if (it->get_audio_object_id() == default_audio_device_object_id)\n            {\n                ca_devices.AddMember(rapidjson::Value(for_input ? \"default_input_device\" : \"default_output_device\", allocator).Move(),\n                                     rapidjson::Value(static_cast<uint64_t>(std::distance(audio_devices.begin(), it))).Move(), allocator);\n                return;\n            }\n        }\n\n        ELKLOG_LOG_ERROR(\"Could not retrieve Apple CoreAudio default {} device\", for_input ? \"input\" : \"output\");\n    };\n\n    add_default_device_index(true);\n    add_default_device_index(false);\n\n    document.AddMember(rapidjson::Value(\"apple_coreaudio_devices\", allocator), ca_devices.Move(), allocator);\n\n    return document;\n}\n\n} // end namespace sushi\n\n#endif // SUSHI_BUILD_WITH_APPLE_COREAUDIO\n\n#ifndef SUSHI_BUILD_WITH_APPLE_COREAUDIO\n\nnamespace sushi {\n\nrapidjson::Document generate_coreaudio_devices_info_document()\n{\n    rapidjson::Document document;\n\n    return document;\n}\n\n} // end namespace sushi\n\n\n#endif // SUSHI_BUILD_WITH_APPLE_COREAUDIO\n"
  },
  {
    "path": "src/audio_frontends/jack_frontend.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Realtime audio frontend for Jack Audio\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifdef SUSHI_BUILD_WITH_JACK\n#include <jack/midiport.h>\n\n#include \"elklog/static_logger.h\"\n\n#include \"jack_frontend.h\"\n#include \"audio_frontend_internals.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"jack audio\");\n\nAudioFrontendStatus JackFrontend::init(BaseAudioFrontendConfiguration* config)\n{\n    auto ret_code = BaseAudioFrontend::init(config);\n    if (ret_code != AudioFrontendStatus::OK)\n    {\n        return ret_code;\n    }\n\n    auto jack_config = static_cast<JackFrontendConfiguration*>(_config);\n    _autoconnect_ports = jack_config->autoconnect_ports;\n    _engine->set_audio_channels(MAX_FRONTEND_CHANNELS, MAX_FRONTEND_CHANNELS);\n    auto status = _engine->set_cv_input_channels(jack_config->cv_inputs);\n    if (status != engine::EngineReturnStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Setting {} cv inputs failed\", jack_config->cv_inputs);\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    _no_cv_input_ports = jack_config->cv_inputs;\n    status = _engine->set_cv_output_channels(jack_config->cv_outputs);\n    if (status != engine::EngineReturnStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Setting {} cv outputs failed\", jack_config->cv_outputs);\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    _no_cv_output_ports = jack_config->cv_outputs;\n    return setup_client(jack_config->client_name, jack_config->server_name);\n}\n\nvoid JackFrontend::cleanup()\n{\n    if (_client)\n    {\n        jack_client_close(_client);\n        _client = nullptr;\n    }\n    _engine->enable_realtime(false);\n}\n\nvoid JackFrontend::run()\n{\n    _engine->enable_realtime(true);\n    int status = jack_activate(_client);\n    if (status != 0)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to activate Jack client, error {}.\", status);\n    }\n    if (_autoconnect_ports)\n    {\n        connect_ports();\n    }\n}\n\nAudioFrontendStatus JackFrontend::setup_client(const std::string& client_name,\n                                               const std::string& server_name)\n{\n    jack_status_t jack_status;\n    jack_options_t options = JackNullOption;\n    if (!server_name.empty())\n    {\n        ELKLOG_LOG_ERROR(\"Using option JackServerName\");\n        options = JackServerName;\n    }\n    _client = jack_client_open(client_name.c_str(), options, &jack_status, server_name.c_str());\n    if (_client == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to open Jack server, error: {}.\", jack_status);\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    /* Set process callback function */\n    int ret = jack_set_process_callback(_client, rt_process_callback, this);\n    if (ret != 0)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to set Jack callback function, error: {}.\", ret);\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    ret = jack_set_latency_callback(_client, latency_callback, this);\n    if (ret != 0)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to set latency callback function, error: {}.\", ret);\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    auto status = setup_sample_rate();\n    if (status != AudioFrontendStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to setup sample rate handling\");\n        return status;\n    }\n    status = setup_ports();\n    if (status != AudioFrontendStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to setup ports\");\n        return status;\n    }\n    status = setup_cv_ports();\n    if (status != AudioFrontendStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to setup cv ports\");\n        return status;\n    }\n    return AudioFrontendStatus::OK;\n}\n\nAudioFrontendStatus JackFrontend::setup_sample_rate()\n{\n    auto sample_rate = jack_get_sample_rate(_client);\n    ELKLOG_LOG_WARNING_IF(sample_rate != _engine->sample_rate(),\n                          \"Sample rate mismatch between engine ({}) and jack ({}), setting to {}\",\n                          _engine->sample_rate(), sample_rate, sample_rate);\n\n    _set_engine_sample_rate(sample_rate);\n    auto status = jack_set_sample_rate_callback(_client, samplerate_callback, this);\n    if (status != 0)\n    {\n        ELKLOG_LOG_WARNING(\"Setting sample rate callback failed with error {}\", status);\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    return AudioFrontendStatus::OK;\n}\n\nAudioFrontendStatus JackFrontend::setup_ports()\n{\n    int port_no = 0;\n    for (auto& port : _output_ports)\n    {\n        port = jack_port_register (_client,\n                                   std::string(\"audio_output_\" + std::to_string(port_no++)).c_str(),\n                                   JACK_DEFAULT_AUDIO_TYPE,\n                                   JackPortIsOutput,\n                                   0);\n        if (port == nullptr)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to open Jack output port {}.\", port_no - 1);\n            return AudioFrontendStatus::AUDIO_HW_ERROR;\n        }\n    }\n    port_no = 0;\n    for (auto& port : _input_ports)\n    {\n        port = jack_port_register (_client,\n                                   std::string(\"audio_input_\" + std::to_string(port_no++)).c_str(),\n                                   JACK_DEFAULT_AUDIO_TYPE,\n                                   JackPortIsInput,\n                                   0);\n        if (port == nullptr)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to open Jack input port {}.\", port_no - 1);\n            return AudioFrontendStatus::AUDIO_HW_ERROR;\n        }\n    }\n    return AudioFrontendStatus::OK;\n}\n\n\nAudioFrontendStatus JackFrontend::setup_cv_ports()\n{\n    for (int i = 0; i < _no_cv_input_ports; ++i)\n    {\n        _cv_input_ports[i] = jack_port_register (_client,\n                                                 std::string(\"cv_input_\" + std::to_string(i)).c_str(),\n                                                 JACK_DEFAULT_AUDIO_TYPE,\n                                                 JackPortIsInput,\n                                                 0);\n        if (_cv_input_ports[i] == nullptr)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to open Jack cv input port {}.\", i);\n            return AudioFrontendStatus::AUDIO_HW_ERROR;\n        }\n    }\n    for (int i = 0; i < _no_cv_output_ports; ++i)\n    {\n        _cv_output_ports[i] = jack_port_register (_client,\n                                                  std::string(\"cv_output_\" + std::to_string(i)).c_str(),\n                                                  JACK_DEFAULT_AUDIO_TYPE,\n                                                  JackPortIsOutput,\n                                                  0);\n        if (_cv_output_ports[i] == nullptr)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to open Jack cv output port {}.\", i);\n            return AudioFrontendStatus::AUDIO_HW_ERROR;\n        }\n    }\n    return AudioFrontendStatus::OK;\n}\n\n/*\n * Searches for external ports and tries to autoconnect them with sushis ports.\n */\nAudioFrontendStatus JackFrontend::connect_ports()\n{\n    const char** out_ports = jack_get_ports(_client, nullptr, nullptr, JackPortIsPhysical|JackPortIsInput);\n    if (out_ports == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to get ports from Jack.\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    for (int id = 0; id < static_cast<int>(_input_ports.size()); ++id)\n    {\n        if (out_ports[id])\n        {\n            int ret = jack_connect(_client, jack_port_name(_output_ports[id]), out_ports[id]);\n            if (ret != 0)\n            {\n                ELKLOG_LOG_WARNING(\"Failed to connect out port {}, error {}.\", jack_port_name(_output_ports[id]), id);\n            }\n        }\n    }\n    jack_free(out_ports);\n\n    /* Same for input ports */\n    const char** in_ports = jack_get_ports(_client, nullptr, nullptr, JackPortIsPhysical|JackPortIsOutput);\n    if (in_ports == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to get ports from Jack.\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    for (int id = 0; id < static_cast<int>(_input_ports.size()); ++id)\n    {\n        if (in_ports[id])\n        {\n            int ret = jack_connect(_client, jack_port_name(_input_ports[id]), in_ports[id]);\n            if (ret != 0)\n            {\n                ELKLOG_LOG_WARNING(\"Failed to connect port {}, error {}.\", jack_port_name(_input_ports[id]), id);\n            }\n        }\n    }\n    jack_free(in_ports);\n\n    return AudioFrontendStatus::OK;\n}\n\nvoid JackFrontend::_set_engine_sample_rate(float sample_rate)\n{\n    BaseAudioFrontend::_set_engine_sample_rate(sample_rate);\n    _int_sample_rate = std::lround(sample_rate);\n}\n\nint JackFrontend::internal_process_callback(jack_nframes_t framecount)\n{\n    set_flush_denormals_to_zero();\n    if (framecount < 64 || framecount % 64)\n    {\n        ELKLOG_LOG_WARNING(\"Chunk size not a multiple of AUDIO_CHUNK_SIZE. Skipping.\");\n        return 0;\n    }\n    jack_nframes_t \tcurrent_frames{0};\n    jack_time_t \tcurrent_usecs{0};\n    jack_time_t \tnext_usecs{0};\n    float           period_usec{0.0};\n    if (jack_get_cycle_times(_client, &current_frames, &current_usecs, &next_usecs, &period_usec) > 0)\n    {\n        ELKLOG_LOG_ERROR(\"Error getting time from jack frontend\");\n    }\n\n    Time start_time = std::chrono::microseconds(current_usecs);\n    if (_start_frame == 0 && current_frames > 0)\n    {\n        _start_frame = current_frames;\n    }\n\n    _handle_resume(start_time, framecount);\n\n    /* Process in chunks of AUDIO_CHUNK_SIZE */\n    for (jack_nframes_t frame = 0; frame < framecount; frame += AUDIO_CHUNK_SIZE)\n    {\n        Time delta_time = std::chrono::microseconds((frame * 1'000'000) / _int_sample_rate);\n        process_audio(frame, AUDIO_CHUNK_SIZE, start_time + delta_time, current_frames + frame - _start_frame);\n    }\n\n    _handle_pause(start_time);\n    return 0;\n}\n\nint JackFrontend::internal_samplerate_callback(jack_nframes_t sample_rate)\n{\n    /* It's not fully clear if this is needed since the sample rate can't\n     * change without restarting the Jack server. Thought is's hinted that\n     * this could be called with a different sample rated than the one\n     * requested if the interface doesn't support it. */\n\n    ELKLOG_LOG_INFO(\"Received a sample rate change from Jack ({})\", sample_rate);\n    _set_engine_sample_rate(sample_rate);\n    return 0;\n}\n\nvoid JackFrontend::internal_latency_callback(jack_latency_callback_mode_t mode)\n{\n    /* Currently, all we want to know is the output latency to a physical\n     * audio output.\n     * We also don't support individual latency compensation on ports, so\n     * we get the maximum latency and pass that on to Sushi. */\n    if (mode == JackPlaybackLatency)\n    {\n        int sample_latency = 0;\n        jack_latency_range_t range;\n        for (auto& port : _output_ports)\n        {\n            jack_port_get_latency_range(port, JackPlaybackLatency, &range);\n            sample_latency = std::max(sample_latency, static_cast<int>(range.max));\n        }\n        Time latency = std::chrono::microseconds((sample_latency * 1'000'000) / _int_sample_rate);\n        _engine->set_output_latency(latency);\n        ELKLOG_LOG_INFO(\"Updated output latency: {} samples, {} ms\", sample_latency, latency.count() / 1000.0f);\n    }\n}\n\nvoid inline JackFrontend::process_audio(jack_nframes_t start_frame, jack_nframes_t framecount,\n                                        Time timestamp, int64_t samplecount)\n{\n    /* Copy jack buffer data to internal buffers */\n    for (size_t i = 0; i < _input_ports.size(); ++i)\n    {\n        float* in_data = static_cast<float*>(jack_port_get_buffer(_input_ports[i], framecount)) + start_frame;\n        std::copy(in_data, in_data + AUDIO_CHUNK_SIZE, _in_buffer.channel(i));\n    }\n    for (int i = 0; i < _no_cv_input_ports; ++i)\n    {\n        float* in_data = static_cast<float*>(jack_port_get_buffer(_cv_input_ports[i], framecount)) + start_frame;\n        _in_controls.cv_values[i] = map_audio_to_cv(in_data[AUDIO_CHUNK_SIZE - 1]);\n    }\n\n    _out_buffer.clear();\n\n    if (_pause_manager.should_process())\n    {\n        _engine->process_chunk(&_in_buffer, &_out_buffer, &_in_controls, &_out_controls, timestamp, samplecount);\n        if (_pause_manager.should_ramp())\n        {\n            _pause_manager.ramp_output(_out_buffer);\n        }\n    }\n\n    for (size_t i = 0; i < _input_ports.size(); ++i)\n    {\n        float* out_data = static_cast<float*>(jack_port_get_buffer(_output_ports[i], framecount)) + start_frame;\n        std::copy(_out_buffer.channel(i), _out_buffer.channel(i) + AUDIO_CHUNK_SIZE, out_data);\n    }\n    /* The jack frontend both inputs and outputs cv in audio range [-1, 1] */\n    for (int i = 0; i < _no_cv_output_ports; ++i)\n    {\n        float* out_data = static_cast<float*>(jack_port_get_buffer(_cv_output_ports[i], framecount)) + start_frame;\n        _cv_output_hist[i] = ramp_cv_output(out_data, _cv_output_hist[i], map_cv_to_audio(_out_controls.cv_values[i]));\n    }\n}\n\n} // end namespace sushi::internal::audio_frontend\n\n#endif\n\n#ifndef SUSHI_BUILD_WITH_JACK\n\n#include \"elklog/static_logger.h\"\n\n#include \"audio_frontends/jack_frontend.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nELKLOG_GET_LOGGER;\n\nJackFrontend::JackFrontend(engine::BaseEngine* engine) : BaseAudioFrontend(engine) {}\n\nAudioFrontendStatus JackFrontend::init(BaseAudioFrontendConfiguration*)\n{\n    /* The log print needs to be in a cpp file for initialisation order reasons */\n    ELKLOG_LOG_ERROR(\"Sushi was not built with Jack support!\");\n    return AudioFrontendStatus::AUDIO_HW_ERROR;\n}\n\n} // end namespace sushi::internal::audio_frontend\n\n#endif\n"
  },
  {
    "path": "src/audio_frontends/jack_frontend.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n * @brief Realtime audio frontend for Jack Audio\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_JACK_FRONTEND_H\n#define SUSHI_JACK_FRONTEND_H\n#ifdef SUSHI_BUILD_WITH_JACK\n\n#include <string>\n#include <memory>\n\n#include <jack/jack.h>\n#include \"twine/twine.h\"\n\n#include \"base_audio_frontend.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nstruct JackFrontendConfiguration : public BaseAudioFrontendConfiguration\n{\n    JackFrontendConfiguration(const std::string& client_name,\n                              const std::string& server_name,\n                              bool autoconnect_ports,\n                              int cv_inputs,\n                              int cv_outputs) :\n            BaseAudioFrontendConfiguration(cv_inputs, cv_outputs),\n            client_name(client_name),\n            server_name(server_name),\n            autoconnect_ports(autoconnect_ports)\n    {}\n\n    virtual ~JackFrontendConfiguration() = default;\n\n    std::string client_name;\n    std::string server_name;\n    bool autoconnect_ports;\n};\nclass JackFrontendAccessor;\nclass JackFrontend : public BaseAudioFrontend\n{\npublic:\n    JackFrontend(engine::BaseEngine* engine) : BaseAudioFrontend(engine) {}\n\n    virtual ~JackFrontend()\n    {\n        cleanup();\n    }\n\n    /**\n     * @brief The realtime process callback given to jack and which will be\n     *        called for every processing chunk.\n     * @param nframes Number of frames in this processing chunk.\n     * @param arg In this case is a pointer to the JackFrontend instance.\n     * @return\n     */\n    static int rt_process_callback(jack_nframes_t nframes, void *arg)\n    {\n        return static_cast<JackFrontend*>(arg)->internal_process_callback(nframes);\n    }\n\n    /**\n     * @brief Callback for sample rate changes\n     * @param nframes New samplerate in Samples per second\n     * @param arg Pointer to the JackFrontend instance.\n     * @return\n     */\n    static int samplerate_callback(jack_nframes_t nframes, void *arg)\n    {\n        return static_cast<JackFrontend*>(arg)->internal_samplerate_callback(nframes);\n    }\n\n    static void latency_callback(jack_latency_callback_mode_t mode, void *arg)\n    {\n        return static_cast<JackFrontend*>(arg)->internal_latency_callback(mode);\n    }\n\n    /**\n     * @brief Initialize the frontend and setup Jack client.\n     * @param config Configuration struct\n     * @return OK on successful initialization, error otherwise.\n     */\n    AudioFrontendStatus init(BaseAudioFrontendConfiguration* config) override;\n\n    /**\n     * @brief Call to clean up resources and release ports\n     */\n    void cleanup() override;\n\n    /**\n     * @brief Activate the realtime frontend, currently blocking.\n     */\n    void run() override;\n\nprivate:\n    friend JackFrontendAccessor;\n    void _set_engine_sample_rate(float sample_rate) override;\n\n    /* Set up the jack client and associated ports */\n    AudioFrontendStatus setup_client(const std::string& client_name, const std::string& server_name);\n    AudioFrontendStatus setup_sample_rate();\n    AudioFrontendStatus setup_ports();\n    AudioFrontendStatus setup_cv_ports();\n    /* Call after activation to connect the frontend ports to system ports */\n    AudioFrontendStatus connect_ports();\n\n    /* Internal process callback function */\n    int internal_process_callback(jack_nframes_t framecount);\n    int internal_samplerate_callback(jack_nframes_t sample_rate);\n    void internal_latency_callback(jack_latency_callback_mode_t mode);\n\n    void process_audio(jack_nframes_t start_frame, jack_nframes_t framecount, Time timestamp, int64_t samplecount);\n\n    std::array<jack_port_t*, MAX_FRONTEND_CHANNELS> _input_ports;\n    std::array<jack_port_t*, MAX_FRONTEND_CHANNELS> _output_ports;\n    std::array<jack_port_t*, MAX_ENGINE_CV_IO_PORTS> _cv_input_ports;\n    std::array<jack_port_t*, MAX_ENGINE_CV_IO_PORTS> _cv_output_ports;\n    std::array<float, MAX_ENGINE_CV_IO_PORTS> _cv_output_hist{0};\n    int _no_cv_input_ports;\n    int _no_cv_output_ports;\n\n    jack_client_t* _client{nullptr};\n    jack_nframes_t _int_sample_rate;\n    jack_nframes_t _start_frame{0};\n    bool _autoconnect_ports{false};\n\n    SampleBuffer<AUDIO_CHUNK_SIZE> _in_buffer{MAX_FRONTEND_CHANNELS};\n    SampleBuffer<AUDIO_CHUNK_SIZE> _out_buffer{MAX_FRONTEND_CHANNELS};\n    engine::ControlBuffer          _in_controls;\n    engine::ControlBuffer          _out_controls;\n};\n\n} // end namespace sushi::internal::jack_frontend\n\n#endif // SUSHI_BUILD_WITH_JACK\n#ifndef SUSHI_BUILD_WITH_JACK\n/* If Jack is disabled in the build config, the jack frontend is replaced with\n   this dummy frontend whose only purpose is to assert if you try to use it */\n#include <string>\n#include \"base_audio_frontend.h\"\n#include \"engine/midi_dispatcher.h\"\nnamespace sushi::internal::audio_frontend {\n\nstruct JackFrontendConfiguration : public BaseAudioFrontendConfiguration\n{\n    JackFrontendConfiguration(const std::string&,\n                              const std::string&,\n                              bool, int, int) : BaseAudioFrontendConfiguration(0, 0) {}\n};\n\nclass JackFrontend : public BaseAudioFrontend\n{\npublic:\n    explicit JackFrontend(engine::BaseEngine* engine);\n    AudioFrontendStatus init(BaseAudioFrontendConfiguration*) override;\n    void cleanup() override {}\n    void run() override {}\n    void pause([[maybe_unused]] bool enabled) override {}\n};\n\n} // end namespace sushi::internal::jack_frontend\n\n#endif\n\n#endif // SUSHI_JACK_FRONTEND_H\n"
  },
  {
    "path": "src/audio_frontends/offline_frontend.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Offline frontend to process audio files in chunks\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cmath>\n#include <cstring>\n#include <random>\n\n#include \"elklog/static_logger.h\"\n\n#include \"offline_frontend.h\"\n\n#include \"audio_frontend_internals.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"offline audio\");\n\n#if defined(__clang__)  || defined(_MSC_VER)\nconstexpr float INPUT_NOISE_LEVEL = 0.06309573444801933f; // pow(10, (-24.0f/20.0f)) pre-computed\n#elif defined(__GNUC__) || defined(__GNUG__)\nconstexpr float INPUT_NOISE_LEVEL = powf(10, (-24.0f/20.0f));\n#endif\n\nconstexpr int   NOISE_SEED = 5; // Using a constant seed makes potential errors reproducible\n\ntemplate<class random_device, class random_dist>\nvoid fill_buffer_with_noise(ChunkSampleBuffer& buffer, random_device& dev, random_dist& dist)\n{\n    for (int c = 0; c < buffer.channel_count(); ++c)\n    {\n        float* channel = buffer.channel(c);\n        for (int i = 0; i < AUDIO_CHUNK_SIZE; ++i)\n        {\n            channel[i] = dist(dev);\n        }\n    }\n}\n\ntemplate<class random_device, class random_dist>\nvoid fill_cv_buffer_with_noise(engine::ControlBuffer& buffer, random_device& dev, random_dist& dist)\n{\n    for (auto& cv : buffer.cv_values)\n    {\n        cv = map_audio_to_cv(dist(dev));\n    }\n}\n\nAudioFrontendStatus OfflineFrontend::init(BaseAudioFrontendConfiguration* config)\n{\n    auto ret_code = BaseAudioFrontend::init(config);\n    if (ret_code != AudioFrontendStatus::OK)\n    {\n        return ret_code;\n    }\n\n    auto off_config = static_cast<OfflineFrontendConfiguration*>(_config);\n    _dummy_mode = off_config->dummy_mode;\n\n    if (_dummy_mode == false)\n    {\n        // Open audio file and check channels / sample rate\n        memset(&_soundfile_info, 0, sizeof(_soundfile_info));\n\n        _input_file = sf_open(off_config->input_filename.c_str(), SFM_READ, &_soundfile_info);\n\n        if (!_input_file)\n        {\n            cleanup();\n            ELKLOG_LOG_ERROR(\"Unable to open input file {}\", off_config->input_filename);\n            return AudioFrontendStatus::INVALID_INPUT_FILE;\n        }\n        _mono = _soundfile_info.channels == 1;\n        auto sample_rate_file = _soundfile_info.samplerate;\n\n        ELKLOG_LOG_WARNING_IF(sample_rate_file != _engine->sample_rate(),\n                              \"Sample rate mismatch between file ({}) and engine ({})\",\n                              sample_rate_file, _engine->sample_rate());\n\n        _set_engine_sample_rate(static_cast<float>(sample_rate_file));\n\n        // Open output file with same format as input file\n\n        _output_file = sf_open(off_config->output_filename.c_str(), SFM_WRITE, &_soundfile_info);\n\n        if (!_output_file)\n        {\n            cleanup();\n            ELKLOG_LOG_ERROR(\"Unable to open output file {}\", off_config->output_filename);\n            return AudioFrontendStatus::INVALID_OUTPUT_FILE;\n        }\n        _engine->set_audio_channels(OFFLINE_FRONTEND_CHANNELS, OFFLINE_FRONTEND_CHANNELS);\n    }\n    else\n    {\n        _engine->set_audio_channels(DUMMY_FRONTEND_CHANNELS, DUMMY_FRONTEND_CHANNELS);\n    }\n    \n    auto status = _engine->set_cv_input_channels(off_config->cv_inputs);\n    if (status != engine::EngineReturnStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Setting {} cv inputs failed\", off_config->cv_inputs);\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    status = _engine->set_cv_output_channels(off_config->cv_outputs);\n    if (status != engine::EngineReturnStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Setting {} cv outputs failed\", off_config->cv_outputs);\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    _engine->set_output_latency(std::chrono::microseconds(0));\n\n    return ret_code;\n}\n\nvoid OfflineFrontend::add_sequencer_events(std::vector<std::unique_ptr<Event>> events)\n{\n    // Sort events by reverse time\n    std::sort(events.begin(), events.end(), [](const std::unique_ptr<Event>& lhs, const std::unique_ptr<Event>& rhs)\n                                              {\n                                                  return lhs->time() > rhs->time();\n                                              });\n    _event_queue = std::move(events);\n}\n\nvoid OfflineFrontend::cleanup()\n{\n    _running = false;\n    if (_worker.joinable())\n    {\n        _worker.join();\n    }\n    if (_input_file)\n    {\n        sf_close(_input_file);\n        _input_file = nullptr;\n    }\n    if (_output_file)\n    {\n        sf_close(_output_file);\n        _output_file = nullptr;\n    }\n}\n\nint time_to_sample_offset(Time chunk_end_time, Time event_time, float samplerate)\n{\n    Time chunktime = std::chrono::microseconds(static_cast<uint64_t>(1'000'000.f * AUDIO_CHUNK_SIZE / samplerate));\n    return AUDIO_CHUNK_SIZE - static_cast<int>((AUDIO_CHUNK_SIZE * (chunk_end_time - event_time) / chunktime));\n}\n\nvoid OfflineFrontend::run()\n{\n    if (_dummy_mode)\n    {\n        _worker = std::thread(&OfflineFrontend::_process_dummy, this);\n    }\n    else\n    {\n        _run_blocking();\n    }\n}\n\nvoid OfflineFrontend::pause(bool /*paused*/)\n{\n    // Currently a no-op\n}\n\n// Process all events up until end_time\nvoid OfflineFrontend::_process_events(Time end_time)\n{\n    while (!_event_queue.empty() && _event_queue.back()->time() < end_time)\n    {\n        auto next_event = std::move(_event_queue.back());\n\n        if (next_event->maps_to_rt_event())\n        {\n            int offset = time_to_sample_offset(end_time, next_event->time(), _engine->sample_rate());\n            auto rt_event = next_event->to_rt_event(offset);\n            _engine->send_rt_event_to_processor(rt_event);\n        }\n\n        _event_queue.pop_back();\n    }\n}\n\nvoid OfflineFrontend::_process_dummy()\n{\n    set_flush_denormals_to_zero();\n    int samplecount = 0;\n    double usec_time = 0.0f;\n    Time start_time = std::chrono::microseconds(0);\n\n    std::ranlux24 rand_gen;\n    rand_gen.seed(NOISE_SEED);\n    std::normal_distribution<float> normal_dist(0.0f, INPUT_NOISE_LEVEL);\n\n    while (_running)\n    {\n        auto process_time = start_time + std::chrono::microseconds(static_cast<uint64_t>(usec_time));\n\n        samplecount += AUDIO_CHUNK_SIZE;\n        usec_time += AUDIO_CHUNK_SIZE * 1'000'000.f / _engine->sample_rate();\n\n        Time chunk_end_time = start_time + std::chrono::microseconds(static_cast<uint64_t>(usec_time));\n        _process_events(chunk_end_time);\n\n        fill_buffer_with_noise(_buffer, rand_gen, normal_dist);\n        fill_cv_buffer_with_noise(_control_buffer, rand_gen, normal_dist);\n        _engine->process_chunk(&_buffer, &_buffer, &_control_buffer, &_control_buffer, process_time, samplecount);\n    }\n}\n\nvoid OfflineFrontend::_run_blocking()\n{\n    set_flush_denormals_to_zero();\n    int samplecount = 0;\n    double usec_time = 0.0f;\n    Time start_time = std::chrono::microseconds(0);\n\n    float file_buffer[OFFLINE_FRONTEND_CHANNELS * AUDIO_CHUNK_SIZE];\n\n    ELK_PUSH_WARNING\n    ELK_DISABLE_ASSIGNMENT_WITHIN_CONDITIONAL\n\n    while (int readcount = static_cast<int>(sf_readf_float(_input_file,\n                                                         file_buffer,\n                                                         static_cast<sf_count_t>(AUDIO_CHUNK_SIZE))))\n    {\n        auto process_time = start_time + std::chrono::microseconds(static_cast<uint64_t>(usec_time));\n\n        samplecount += readcount;\n        usec_time += readcount * 1'000'000.f / _engine->sample_rate();\n\n        Time chunk_end_time = start_time + std::chrono::microseconds(static_cast<uint64_t>(usec_time));\n        _process_events(chunk_end_time);\n\n        _buffer.clear();\n\n        if (_mono)\n        {\n            std::copy(file_buffer, file_buffer + AUDIO_CHUNK_SIZE, _buffer.channel(0));\n        }\n        else\n        {\n            auto buffer = ChunkSampleBuffer::create_non_owning_buffer(_buffer, 0, 2);\n            buffer.from_interleaved(file_buffer);\n        }\n        /* Gate and CV are ignored when using file frontend */\n        _engine->process_chunk(&_buffer, &_buffer, &_control_buffer, &_control_buffer, process_time, samplecount);\n\n        if (_mono)\n        {\n            std::copy(_buffer.channel(0), _buffer.channel(0) + AUDIO_CHUNK_SIZE, file_buffer );\n        }\n        else\n        {\n            auto buffer = ChunkSampleBuffer::create_non_owning_buffer(_buffer, 0, 2);\n            buffer.to_interleaved(file_buffer);\n        }\n\n        // Write to file\n        // Should we check the number of samples effectively written?\n        // Not done in libsndfile's example\n        sf_writef_float(_output_file, file_buffer, static_cast<sf_count_t>(readcount));\n    }\n\n    ELK_POP_WARNING\n}\n\n} // end namespace sushi::internal::audio_frontend\n"
  },
  {
    "path": "src/audio_frontends/offline_frontend.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n  * @brief Offline frontend to process audio files in chunks\n  * @Copyright 2017-2023 Elk Audio AB, Stockholm\n  */\n\n#ifndef SUSHI_OFFLINE_FRONTEND_H\n#define SUSHI_OFFLINE_FRONTEND_H\n\n#include <string>\n#include <vector>\n#include <atomic>\n#include <thread>\n\n#include <sndfile.h>\n\n#include \"base_audio_frontend.h\"\n#include \"library/rt_event.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nconstexpr int OFFLINE_FRONTEND_CHANNELS = 2;\nconstexpr int DUMMY_FRONTEND_CHANNELS = 10;\n\nstruct OfflineFrontendConfiguration : public BaseAudioFrontendConfiguration\n{\n    OfflineFrontendConfiguration(std::string input_filename,\n                                 std::string output_filename,\n                                 bool dummy_mode,\n                                 int cv_inputs,\n                                 int cv_outputs) :\n            BaseAudioFrontendConfiguration(cv_inputs, cv_outputs),\n            input_filename(std::move(input_filename)),\n            output_filename(std::move(output_filename)),\n            dummy_mode(dummy_mode)\n    {}\n\n    ~OfflineFrontendConfiguration() override = default;\n\n    std::string input_filename;\n    std::string output_filename;\n    bool dummy_mode;\n};\n\nclass OfflineFrontendAccessor;\n\nclass OfflineFrontend : public BaseAudioFrontend\n{\npublic:\n    explicit OfflineFrontend(engine::BaseEngine* engine) : BaseAudioFrontend(engine),\n                                                           _input_file(nullptr),\n                                                           _output_file(nullptr),\n                                                           _running{true}\n    {\n        _buffer.clear();\n    }\n\n    ~OfflineFrontend() override\n    {\n        cleanup();\n    }\n\n    AudioFrontendStatus init(BaseAudioFrontendConfiguration* config) override;\n\n    /**\n     * @brief Add events that should be run during the processing\n     * @param An std::vector containing the timestamped events.\n     */\n    void add_sequencer_events(std::vector<std::unique_ptr<Event>> events);\n\n    void cleanup() override;\n\n    void run() override;\n\n    void pause(bool paused) override;\n\nprivate:\n    friend OfflineFrontendAccessor;\n\n    void _process_events(Time end_time);\n    void _process_dummy();\n    void _run_blocking();\n\n    SNDFILE*            _input_file;\n    SNDFILE*            _output_file;\n    SF_INFO             _soundfile_info;\n    bool                _mono;\n    bool                _dummy_mode;\n    std::atomic_bool    _running;\n    std::thread         _worker;\n\n    SampleBuffer<AUDIO_CHUNK_SIZE> _buffer {DUMMY_FRONTEND_CHANNELS};\n    engine::ControlBuffer _control_buffer;\n\n    std::vector<std::unique_ptr<Event>> _event_queue;\n};\n\n} // end namespace sushi::internal::audio_frontend\n\n#endif // SUSHI_OFFLINE_FRONTEND_H\n"
  },
  {
    "path": "src/audio_frontends/portaudio_devices_dump.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utility functions for dumping Portaudio devices info\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"sushi/portaudio_devices_dump.h\"\n\n#include \"portaudio_frontend.h\"\n\n\nnamespace sushi {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"portaudio\");\n\n/**\n * @brief Retrieve Portaudio's registered devices information.\n *        Can be queried before instantiating an actual PortaudioFrontend\n *\n * @return Device information list in JSON format\n */\nrapidjson::Document generate_portaudio_devices_info_document()\n{\n    sushi::internal::audio_frontend::PortAudioFrontend frontend {nullptr};\n\n    rapidjson::Document document;\n    document.SetObject();\n    rapidjson::Document::AllocatorType& allocator = document.GetAllocator();\n\n    auto n_devs = frontend.devices_count();\n    if ( !n_devs.has_value() || (n_devs.value() <= 0) )\n    {\n        ELKLOG_LOG_ERROR(\"No Portaudio devices found\");\n        return document;\n    }\n\n    rapidjson::Value pa_devices(rapidjson::kObjectType);\n    rapidjson::Value devices(rapidjson::kArrayType);\n    for (int i = 0; i < n_devs.value(); i++)\n    {\n        auto devinfo = frontend.device_info(i);\n        if (!devinfo.has_value())\n        {\n            ELKLOG_LOG_ERROR(\"Could not retrieve device info for Portaudio device with idx: {}\", i);\n            continue;\n        }\n        auto di = devinfo.value();\n\n        rapidjson::Value device_obj(rapidjson::kObjectType);\n        device_obj.AddMember(rapidjson::Value(\"id\", allocator).Move(),\n                             rapidjson::Value(i).Move(), allocator);\n        device_obj.AddMember(rapidjson::Value(\"name\", allocator).Move(),\n                             rapidjson::Value(di.name.c_str(), allocator).Move(), allocator);\n        device_obj.AddMember(rapidjson::Value(\"host api\", allocator).Move(),\n                             rapidjson::Value(di.host_api.c_str(), allocator).Move(), allocator);\n        device_obj.AddMember(rapidjson::Value(\"inputs\", allocator).Move(),\n                             rapidjson::Value(di.inputs).Move(), allocator);\n        device_obj.AddMember(rapidjson::Value(\"outputs\", allocator).Move(),\n                             rapidjson::Value(di.outputs).Move(), allocator);\n        devices.PushBack(device_obj.Move(), allocator);\n    }\n    pa_devices.AddMember(rapidjson::Value(\"devices\", allocator).Move(), devices.Move(), allocator);\n\n    auto default_input = frontend.default_input_device();\n    if (! default_input.has_value() )\n    {\n        ELKLOG_LOG_ERROR(\"Could not retrieve Portaudio default input device\");\n    }\n    else\n    {\n        pa_devices.AddMember(rapidjson::Value(\"default_input_device\", allocator).Move(),\n                             rapidjson::Value(default_input.value()).Move(), allocator);\n    }\n    auto default_output = frontend.default_output_device();\n    if (! default_output.has_value() )\n    {\n        ELKLOG_LOG_ERROR(\"Could not retrieve Portaudio default output device\");\n    }\n    else\n    {\n        pa_devices.AddMember(rapidjson::Value(\"default_output_device\", allocator).Move(),\n                             rapidjson::Value(default_output.value()).Move(), allocator);\n    }\n\n    document.AddMember(rapidjson::Value(\"portaudio_devices\", allocator), pa_devices.Move(), allocator);\n\n    return document;\n}\n\n} // end namespace sushi\n\n"
  },
  {
    "path": "src/audio_frontends/portaudio_frontend.cpp",
    "content": "/*\n * Copyright 2017-2024 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Realtime audio frontend for PortAudio\n * @Copyright 2017-2024 Elk Audio AB, Stockholm\n */\n\n#ifdef SUSHI_BUILD_WITH_PORTAUDIO\n\n#include <cstring>\n\n#include \"elklog/static_logger.h\"\n\n#include \"portaudio_frontend.h\"\n\n#include \"audio_frontend_internals.h\"\n\n#ifdef _MSC_VER\n    #define bzero(b, len) (memset((b), '\\0', (len)), (void) 0)\n#endif\n\nnamespace sushi::internal::audio_frontend {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"portaudio\");\n\nstd::optional<std::string> get_portaudio_output_device_name(std::optional<int> portaudio_output_device_id)\n{\n    int device_index = -1;\n\n    if (portaudio_output_device_id.has_value())\n    {\n        device_index = portaudio_output_device_id.value();\n    }\n    else\n    {\n        device_index = Pa_GetDefaultOutputDevice();\n    }\n\n    sushi::internal::audio_frontend::PortAudioFrontend frontend {nullptr};\n\n    auto device_info = frontend.device_info(device_index);\n\n    if (!device_info.has_value())\n    {\n        ELKLOG_LOG_ERROR(\"Could not retrieve device info for Portaudio device with idx: {}\", device_index);\n        return std::nullopt;\n    }\n\n    return device_info.value().name;\n}\n\nAudioFrontendStatus PortAudioFrontend::init(BaseAudioFrontendConfiguration* config)\n{\n    auto portaudio_config = static_cast<PortAudioFrontendConfiguration*>(config);\n\n    auto ret_code = BaseAudioFrontend::init(config);\n    if (ret_code != AudioFrontendStatus::OK)\n    {\n        return ret_code;\n    }\n\n    ret_code = _initialize_portaudio();\n    if (ret_code != AudioFrontendStatus::OK)\n    {\n        return ret_code;\n    }\n\n    // Setup devices\n    int device_count = Pa_GetDeviceCount();\n    int input_device_id = portaudio_config->input_device_id.value_or(Pa_GetDefaultInputDevice());\n    if (input_device_id >= device_count)\n    {\n        ELKLOG_LOG_ERROR(\"Input device id is out of range\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    // If there is no available input device the default device will\n    // return negative which will cause issues later\n    else if (input_device_id < 0)\n    {\n        input_device_id = 0;\n    }\n\n    int output_device_id = portaudio_config->output_device_id.value_or(Pa_GetDefaultOutputDevice());\n    if (output_device_id >= device_count)\n    {\n        ELKLOG_LOG_ERROR(\"Output device id is out of range\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    // If there is no available output device the default device will\n    // return negative which will cause issues later\n    else if (output_device_id < 0)\n    {\n        output_device_id = 0;\n    }\n\n    _input_device_info = Pa_GetDeviceInfo(input_device_id);\n    _output_device_info = Pa_GetDeviceInfo(output_device_id);\n\n    // Setup audio and CV channels\n    auto channel_conf_result = _configure_audio_channels(portaudio_config);\n    if (channel_conf_result != AudioFrontendStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to configure audio channels\");\n        return channel_conf_result;\n    }\n    ELKLOG_LOG_DEBUG(\"Setting up port audio with {} inputs {} outputs\", _num_total_input_channels, _num_total_output_channels);\n\n    // Setup device parameters\n    PaStreamParameters input_parameters;\n    memset(&input_parameters, 0, sizeof(input_parameters));\n    input_parameters.device = input_device_id;\n    input_parameters.channelCount = _audio_input_channels + _cv_input_channels;\n    input_parameters.sampleFormat = paFloat32;\n    input_parameters.suggestedLatency = portaudio_config->suggested_input_latency;\n    input_parameters.hostApiSpecificStreamInfo = nullptr;\n\n    PaStreamParameters output_parameters;\n    memset(&output_parameters, 0, sizeof(output_parameters));\n    output_parameters.device = output_device_id;\n    output_parameters.channelCount = _audio_output_channels + _cv_output_channels;\n    output_parameters.sampleFormat = paFloat32;\n    output_parameters.suggestedLatency = portaudio_config->suggested_output_latency;\n    output_parameters.hostApiSpecificStreamInfo = nullptr;\n    // Setup samplerate\n    double samplerate = _engine->sample_rate();\n    if (_configure_samplerate(&input_parameters, &output_parameters, &samplerate) == false)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to configure samplerate\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    ELKLOG_LOG_WARNING_IF(samplerate != _engine->sample_rate(),\n                          \"Failed to use engine samplerate ({}), using {} instead\",\n                          _engine->sample_rate(), samplerate);\n\n    _set_engine_sample_rate(static_cast<float>(samplerate));\n\n    // Open the stream\n    // In case there is no input device available we only want to use output\n    auto input_param_ptr = (_audio_input_channels + _cv_input_channels) > 0 ? &input_parameters : NULL;\n    PaError err = Pa_OpenStream(&_stream,\n                                input_param_ptr,\n                                &output_parameters,\n                                samplerate,\n                                AUDIO_CHUNK_SIZE,\n                                paNoFlag,\n                                &rt_process_callback,\n                                static_cast<void*>(this));\n    if (err != paNoError)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to open stream: {}\", Pa_GetErrorText(err));\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    else\n    {\n        _stream_initialized = true;\n    }\n    auto stream_info = Pa_GetStreamInfo(_stream);\n    Time latency = std::chrono::microseconds(static_cast<int>(stream_info->outputLatency * 1'000'000));\n    _engine->set_output_latency(latency);\n\n    _time_offset = Pa_GetStreamTime(_stream);\n    _start_time = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch());\n\n    if (_audio_input_channels + _cv_input_channels > 0)\n    {\n        ELKLOG_LOG_INFO(\"Connected input channels to {}\", _input_device_info->name);\n        ELKLOG_LOG_INFO(\"Input device has {} available channels\", _input_device_info->maxInputChannels);\n    }\n    else\n    {\n        ELKLOG_LOG_DEBUG(\"No input channels found not connecting to input device\");\n    }\n\n    if (_audio_output_channels + _cv_output_channels > 0)\n    {\n        ELKLOG_LOG_DEBUG(\"Connected output channels to {}\", _output_device_info->name);\n        ELKLOG_LOG_INFO(\"Output device has {} available channels\", _output_device_info->maxOutputChannels);\n    }\n    else\n    {\n        ELKLOG_LOG_INFO(\"No output channels found not connecting to output device\");\n        ELKLOG_LOG_INFO(\"Output device has {} available channels\", _output_device_info->maxOutputChannels);\n    }\n    ELKLOG_LOG_INFO(\"Stream started, using input latency {} and output latency {}\", stream_info->inputLatency, stream_info->outputLatency);\n\n    return AudioFrontendStatus::OK;\n}\n\nvoid PortAudioFrontend::cleanup()\n{\n    PaError result;\n    if (_engine != nullptr)\n    {\n        _engine->enable_realtime(false);\n    }\n\n    if (_stream_initialized)\n    {\n        result = Pa_IsStreamActive(_stream);\n        if (result == 1)\n        {\n            ELKLOG_LOG_INFO(\"Closing PortAudio stream\");\n            Pa_StopStream(_stream);\n        }\n        else if ((result != paNoError) && (_engine != nullptr))\n        {\n            ELKLOG_LOG_WARNING(\"Error while checking for active stream: {}\", Pa_GetErrorText(result));\n        }\n    }\n\n    result = Pa_Terminate();\n    if (result != paNoError)\n    {\n        ELKLOG_LOG_WARNING(\"Error while terminating Portaudio: {}\", Pa_GetErrorText(result));\n    }\n}\n\nvoid PortAudioFrontend::run()\n{\n    _engine->enable_realtime(true);\n    Pa_StartStream(_stream);\n}\n\nstd::optional<int> PortAudioFrontend::devices_count()\n{\n    if (! (_initialize_portaudio() == AudioFrontendStatus::OK))\n    {\n        return std::nullopt;\n    }\n\n    int devices = Pa_GetDeviceCount();\n    if (devices < 0)\n    {\n        ELKLOG_LOG_ERROR(\"Error querying portaudio devices: {}\", devices);\n        return std::nullopt;\n    }\n\n    return devices;\n}\n\nstd::optional<PortaudioDeviceInfo> PortAudioFrontend::device_info(int device_idx)\n{\n    if (! (_initialize_portaudio() == AudioFrontendStatus::OK))\n    {\n        return std::nullopt;\n    }\n\n    const PaDeviceInfo* pa_devinfo = Pa_GetDeviceInfo(device_idx);\n    if (!pa_devinfo)\n    {\n        ELKLOG_LOG_ERROR(\"Error querying portaudio devices {}\", device_idx);\n        return std::nullopt;\n    }\n    const PaHostApiInfo* pa_apiinfo = Pa_GetHostApiInfo(pa_devinfo->hostApi);\n    if (!pa_apiinfo)\n    {\n        ELKLOG_LOG_ERROR(\"Error querying portaudio host api {}\", pa_devinfo->hostApi);\n        return std::nullopt;\n    }\n\n    PortaudioDeviceInfo devinfo;\n    devinfo.name = pa_devinfo->name;\n    devinfo.host_api = pa_apiinfo->name;\n    devinfo.inputs = pa_devinfo->maxInputChannels;\n    devinfo.outputs = pa_devinfo->maxOutputChannels;\n\n    return devinfo;\n}\n\nstd::optional<int> PortAudioFrontend::default_input_device()\n{\n    if (! (_initialize_portaudio() == AudioFrontendStatus::OK))\n    {\n        return std::nullopt;\n    }\n\n    int default_input = Pa_GetDefaultInputDevice();\n    if (default_input == paNoDevice)\n    {\n        ELKLOG_LOG_WARNING(\"Could not retrieve default input device\");\n        return std::nullopt;\n    }\n\n    return default_input;\n}\n\nstd::optional<int> PortAudioFrontend::default_output_device()\n{\n    if (! (_initialize_portaudio() == AudioFrontendStatus::OK))\n    {\n        return std::nullopt;\n    }\n\n    int default_output = Pa_GetDefaultOutputDevice();\n    if (default_output == paNoDevice)\n    {\n        ELKLOG_LOG_WARNING(\"Could not retrieve default output device\");\n        return std::nullopt;\n    }\n\n    return default_output;\n}\n\nAudioFrontendStatus PortAudioFrontend::_initialize_portaudio()\n{\n    if (_pa_initialized)\n    {\n        return AudioFrontendStatus::OK;\n    }\n\n    PaError err = Pa_Initialize();\n    if (err != paNoError)\n    {\n        ELKLOG_LOG_ERROR(\"Error initializing PortAudio: {}\", Pa_GetErrorText(err));\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    return AudioFrontendStatus::OK;\n}\n\nAudioFrontendStatus PortAudioFrontend::_configure_audio_channels(const PortAudioFrontendConfiguration* config)\n{\n    if (_input_device_info == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Configure audio channels called before input device info was collected\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    if (_output_device_info == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Configure audio channels called before output device info was collected\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    _num_total_input_channels = _input_device_info->maxInputChannels;\n    _num_total_output_channels = _output_device_info->maxOutputChannels;\n\n    _cv_input_channels = config->cv_inputs;\n    _cv_output_channels = config->cv_outputs;\n    if (_cv_input_channels > _num_total_input_channels)\n    {\n        ELKLOG_LOG_ERROR(\"Requested more CV channels than available input channels\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    if (_cv_output_channels > _num_total_output_channels)\n    {\n        ELKLOG_LOG_ERROR(\"Requested more CV channels than available output channels\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    _audio_input_channels = _num_total_input_channels - _cv_input_channels;\n    _audio_output_channels = _num_total_output_channels - _cv_output_channels;\n    _in_buffer = ChunkSampleBuffer(_audio_input_channels);\n    _out_buffer = ChunkSampleBuffer(_audio_output_channels);\n    _engine->set_audio_channels(_audio_input_channels, _audio_output_channels);\n    auto status = _engine->set_cv_input_channels(_cv_input_channels);\n    if (status != engine::EngineReturnStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to setup CV input channels\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    status = _engine->set_cv_output_channels(_cv_output_channels);\n    if (status != engine::EngineReturnStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to setup CV output channels\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    return AudioFrontendStatus::OK;\n}\n\nbool PortAudioFrontend::_configure_samplerate(const PaStreamParameters* input_parameters,\n                                              const PaStreamParameters* output_parameters,\n                                              double* samplerate)\n{\n    std::array<double, 3> samplerates = {\n        *samplerate,\n        _input_device_info->defaultSampleRate,\n        _output_device_info->defaultSampleRate\n    };\n    PaError result;\n    for (auto sr : samplerates)\n    {\n        result = Pa_IsFormatSupported(input_parameters, output_parameters, sr);\n        if (result != 0)\n        {\n            ELKLOG_LOG_WARNING(\"Error configuring samplerate {}: {}\", sr, Pa_GetErrorText(result));\n        }\n        else\n        {\n            *samplerate = sr;\n            return true;\n        }\n    }\n    return false;\n}\n\nint PortAudioFrontend::_internal_process_callback(const void* input,\n                                                  void* output,\n                                                  unsigned long frame_count,\n                                                  const PaStreamCallbackTimeInfo* time_info,\n                                                  [[maybe_unused]]PaStreamCallbackFlags status_flags)\n{\n    assert(frame_count == AUDIO_CHUNK_SIZE);\n    ELKLOG_LOG_WARNING_IF(status_flags & (paOutputUnderflow | paOutputOverflow | paInputUnderflow | paInputOverflow), \"Portaudio reported under/overflow: {}\", status_flags);\n\n    auto pa_time_elapsed = std::chrono::duration<double>(time_info->outputBufferDacTime - _time_offset);\n    Time timestamp = _start_time + std::chrono::duration_cast<std::chrono::microseconds>(pa_time_elapsed);\n\n    _out_buffer.clear();\n    _handle_resume(timestamp, static_cast<int>(frame_count));\n\n    if (_pause_manager.should_process())\n    {\n        _copy_interleaved_audio(static_cast<const float*>(input));\n        _engine->process_chunk(&_in_buffer, &_out_buffer, &_in_controls, &_out_controls, timestamp, _processed_sample_count);\n        if (_pause_manager.should_ramp())\n        {\n            _pause_manager.ramp_output(_out_buffer);\n        }\n    }\n\n    _handle_pause(timestamp);\n\n    _output_interleaved_audio(static_cast<float*>(output));\n\n    _processed_sample_count += frame_count;\n    return 0;\n}\n\nvoid PortAudioFrontend::_copy_interleaved_audio(const float* input)\n{\n    for (int c = 0; c < _num_total_input_channels; c++)\n    {\n        if (c < _audio_input_channels)\n        {\n            for (size_t s = 0; s < AUDIO_CHUNK_SIZE; s++)\n            {\n                float* in_dst = _in_buffer.channel(c);\n                in_dst[s] = input[s * _num_total_input_channels + c];\n            }\n        }\n        else\n        {\n            int cc = c - _audio_input_channels;\n            _in_controls.cv_values[cc] = map_audio_to_cv(input[AUDIO_CHUNK_SIZE - 1]);\n        }\n    }\n}\n\nvoid PortAudioFrontend::_output_interleaved_audio(float* output)\n{\n    for (int c = 0; c < _num_total_output_channels; c++)\n    {\n        if (c < _audio_output_channels)\n        {\n            for (size_t s = 0; s < AUDIO_CHUNK_SIZE; s++)\n            {\n                const float* out_src = _out_buffer.channel(c);\n                output[s * _num_total_output_channels + c] = out_src[s];\n            }\n        }\n        else\n        {\n            int cc = c - _audio_output_channels;\n            _cv_output_his[cc] = ramp_cv_output(output, _cv_output_his[cc], map_cv_to_audio(_out_controls.cv_values[cc]));\n        }\n    }\n}\n\n} // end namespace sushi::internal::audio_frontend\n\n#endif\n#ifndef SUSHI_BUILD_WITH_PORTAUDIO\n\n#include \"elklog/static_logger.h\"\n\n#include \"audio_frontends/portaudio_frontend.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nELKLOG_GET_LOGGER;\nPortAudioFrontend::PortAudioFrontend(engine::BaseEngine* engine) : BaseAudioFrontend(engine)\n{}\nAudioFrontendStatus PortAudioFrontend::init(BaseAudioFrontendConfiguration*)\n{\n    /* The log print needs to be in a cpp file for initialisation order reasons */\n    ELKLOG_LOG_ERROR(\"Sushi was not built with PortAudio support!\");\n    return AudioFrontendStatus::AUDIO_HW_ERROR;\n}\n\n}\n#endif\n"
  },
  {
    "path": "src/audio_frontends/portaudio_frontend.h",
    "content": "/*\n * Copyright 2017-2024 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n  * @brief Realtime audio frontend for PortAudio\n  * @Copyright 2017-2024 Elk Audio AB, Stockholm\n  */\n\n#ifndef SUSHI_PORTAUDIO_FRONTEND_H\n#define SUSHI_PORTAUDIO_FRONTEND_H\n\n#ifdef SUSHI_BUILD_WITH_PORTAUDIO\n\n#include <string>\n#include <memory>\n#include <optional>\n\n// TODO: Keep an eye on these deprecated declarations and update when they are fixed.\n// There is an open issue on github at the time of writing about C11 which would fix this.\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DEPRECATED_DECLARATIONS\n#include <portaudio.h>\nELK_POP_WARNING\n\n#include \"base_audio_frontend.h\"\n\nnamespace sushi::internal::audio_frontend {\n\n\n#ifdef SUSHI_BUILD_WITH_PORTAUDIO\n\n/**\n * @brief Given an optional portaudio output device ID, this attempts to fetch the corresponding name.\n *        If no id is passed (the optional argument has no value), the default output device is used.\n * @param portaudio_output_device_id an optional int device id.\n * @return An optional std::string, if a value can be resolved.\n */\n[[nodiscard]] std::optional<std::string> get_portaudio_output_device_name(std::optional<int> portaudio_output_device_id);\n\n#endif\n\nstruct PortaudioDeviceInfo\n{\n    std::string name;\n    std::string host_api;\n    int inputs;\n    int outputs;\n};\n\nstruct PortAudioFrontendConfiguration : public BaseAudioFrontendConfiguration\n{\n    PortAudioFrontendConfiguration(std::optional<int> input_device_id,\n                                   std::optional<int> output_device_id,\n                                   float suggested_input_latency,\n                                   float suggested_output_latency,\n                                   int cv_inputs,\n                                   int cv_outputs) :\n            BaseAudioFrontendConfiguration(cv_inputs, cv_outputs),\n            input_device_id(input_device_id),\n            output_device_id(output_device_id),\n            suggested_input_latency(suggested_input_latency),\n            suggested_output_latency(suggested_output_latency)\n    {}\n\n    ~PortAudioFrontendConfiguration() override = default;\n\n    std::optional<int> input_device_id;\n    std::optional<int> output_device_id;\n    float suggested_input_latency{0.0f};\n    float suggested_output_latency{0.0f};\n};\n\nclass PortaudioFrontendAccessor;\n\nclass PortAudioFrontend : public BaseAudioFrontend\n{\npublic:\n    explicit PortAudioFrontend(engine::BaseEngine* engine) : BaseAudioFrontend(engine) {}\n\n    ~PortAudioFrontend() override\n    {\n        cleanup();\n    }\n\n    /**\n     * @brief The realtime process callback given to Port Audio which will be\n     *        called for every processing chunk.\n     *\n     * @param input pointer to the interleaved input data. Needs to be cast to the correct sample format\n     * @param output pointer to the interleaved output data. Needs to be cast to the correct sample format\n     * @param frame_count number of frames to process\n     * @param time_info timing information for the buffers passed to the stream callback\n     * @param status_flags is set if under or overflow has occurred\n     * @param user_data  pointer to the PortAudioFrontend instance\n     * @return int\n     */\n    static int rt_process_callback(const void* input,\n                                   void* output,\n                                   unsigned long frame_count,\n                                   const PaStreamCallbackTimeInfo* time_info,\n                                   PaStreamCallbackFlags status_flags,\n                                   void* user_data)\n    {\n        return static_cast<PortAudioFrontend*>(user_data)->_internal_process_callback(input,\n                                                                                      output,\n                                                                                      frame_count,\n                                                                                      time_info,\n                                                                                      status_flags);\n    }\n\n    /**\n     * @brief Initialize the frontend and setup PortAudio client.\n     * @param config Configuration struct\n     * @return OK on successful initialization, error otherwise.\n     */\n    AudioFrontendStatus init(BaseAudioFrontendConfiguration* config) override;\n\n    /**\n     * @brief Call to clean up resources and release ports\n     */\n    void cleanup() override;\n\n    /**\n     * @brief Activate the realtime frontend, currently blocking.\n     */\n    void run() override;\n\n    /**\n     * @brief Get the n. of available devices.\n     *        Can be called before init()\n     *\n     * @return Total number of devices as reported by Portaudio, or std::nullopt if there was an error\n     */\n    std::optional<int> devices_count();\n\n    /**\n     * @brief Query a device basic properties\n     *\n     * @param device_idx Device index in [0..devices_count()-1]\n     *\n     * @return Struct with device name, n. of input channels, n. of output channels.\n     *         std::nullopt is returned if there was an error\n     */\n    std::optional<PortaudioDeviceInfo> device_info(int device_idx);\n\n    /**\n     * @brief Query the default input device\n     *\n     * @return Index in [0..devices_count()-1], or std::nullopt if there was an error\n     */\n    std::optional<int> default_input_device();\n\n    /**\n     * @brief Query the default output device\n     *\n     * @return Index in [0..devices_count()-1], or std::nullopt if there was an error\n     */\n    std::optional<int> default_output_device();\n\nprivate:\n    friend PortaudioFrontendAccessor;\n\n    /**\n     * @brief Initialize PortAudio engine, and cache the result to avoid multiple initializations\n     *\n     * @return OK or AUDIO_HW_ERROR\n     */\n    AudioFrontendStatus _initialize_portaudio();\n\n    AudioFrontendStatus _configure_audio_channels(const PortAudioFrontendConfiguration* config);\n\n    /**\n     * @brief Configure the samplerate to use. First tests if the value of the given\n     * samplerate is compatible with the input and output parameters. If not it will test\n     * the default samplerate of the input and output device respectively. The value of\n     * the samplerate parameter will be set to the samplerate that was found to work.\n     *\n     * @param input_parameters input configuration to test against the samplerate\n     * @param output_parameters output configuration to test against the samplerate\n     * @param samplerate pointer to a float containing the first samplerate to test\n     * @return true if a samplerate was found\n     * @return false if no samplerate was found\n     */\n    bool _configure_samplerate(const PaStreamParameters* input_parameters,\n                               const PaStreamParameters* output_parameters,\n                               double* samplerate);\n\n    int _internal_process_callback(const void* input,\n                                   void* output,\n                                   unsigned long frame_count,\n                                   const PaStreamCallbackTimeInfo* time_info,\n                                   PaStreamCallbackFlags status_flags);\n\n    void _copy_interleaved_audio(const float* input);\n\n    void _output_interleaved_audio(float* output);\n\n    std::array<float, MAX_ENGINE_CV_IO_PORTS> _cv_output_his {0};\n    int _num_total_input_channels {0};\n    int _num_total_output_channels {0};\n    int _audio_input_channels {0};\n    int _audio_output_channels {0};\n    int _cv_input_channels {0};\n    int _cv_output_channels {0};\n\n    bool _pa_initialized {false};\n    PaStream* _stream {nullptr};\n\n    // This is convenient mostly for mock testing, where checking for nullptr will not work\n    bool _stream_initialized {false};\n    const PaDeviceInfo* _input_device_info;\n    const PaDeviceInfo* _output_device_info;\n\n    ChunkSampleBuffer _in_buffer;\n    ChunkSampleBuffer _out_buffer;\n\n    Time _start_time{Time(0)};\n    PaTime _time_offset{0};\n    int64_t _processed_sample_count {0};\n\n    engine::ControlBuffer _in_controls;\n    engine::ControlBuffer _out_controls;\n};\n\n} // end namespace sushi::internal::audio_frontend\n\n#endif // SUSHI_BUILD_WITH_PORTAUDIO\n\n#ifndef SUSHI_BUILD_WITH_PORTAUDIO\n/* If PortAudio is disabled in the build config, the PortAudio frontend is replaced with\n   this dummy frontend whose only purpose is to assert if you try to use it */\n#include <string>\n#include \"base_audio_frontend.h\"\n#include \"engine/midi_dispatcher.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nstruct PortAudioFrontendConfiguration : public BaseAudioFrontendConfiguration\n{\n    PortAudioFrontendConfiguration(std::optional<int>, std::optional<int>, float, float, int, int) : BaseAudioFrontendConfiguration(0, 0) {}\n};\n\nstruct PortaudioDeviceInfo\n{\n    std::string name;\n    std::string host_api;\n    int inputs;\n    int outputs;\n};\n\nclass PortAudioFrontend : public BaseAudioFrontend\n{\npublic:\n    PortAudioFrontend(engine::BaseEngine* engine);\n    AudioFrontendStatus init(BaseAudioFrontendConfiguration*) override;\n    void cleanup() override {}\n    void run() override {}\n    void pause([[maybe_unused]] bool enabled) override {}\n    std::optional<int> devices_count() { return 0; }\n    std::optional<PortaudioDeviceInfo> device_info(int /*device_idx*/) { return PortaudioDeviceInfo(); }\n    std::optional<int> default_input_device() { return 0; }\n    std::optional<int> default_output_device() { return 0; }\n};\n\n} // end namespace sushi::internal::audio_frontend\n\n#endif\n\n#endif // SUSHI_PORTAUDIO_FRONTEND_H\n\n"
  },
  {
    "path": "src/audio_frontends/reactive_frontend.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Passive frontend to process audio from a callback through a host application.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <iostream>\n\n#include \"elklog/static_logger.h\"\n\n#include \"reactive_frontend.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"Reactive audio frontend\");\n\nAudioFrontendStatus ReactiveFrontend::init(BaseAudioFrontendConfiguration* config)\n{\n    auto ret_code = BaseAudioFrontend::init(config);\n    if (ret_code != AudioFrontendStatus::OK)\n    {\n        return ret_code;\n    }\n\n    auto frontend_config = static_cast<ReactiveFrontendConfiguration*>(_config); // static cast because of no rtti\n\n    _engine->set_audio_channels(REACTIVE_FRONTEND_CHANNELS, REACTIVE_FRONTEND_CHANNELS);\n\n    auto status = _engine->set_cv_input_channels(frontend_config->cv_inputs);\n    if (status != engine::EngineReturnStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Setting {} cv inputs failed\", frontend_config->cv_inputs);\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    status = _engine->set_cv_output_channels(frontend_config->cv_outputs);\n    if (status != engine::EngineReturnStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Setting {} cv outputs failed\", frontend_config->cv_outputs);\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    _engine->set_output_latency(std::chrono::microseconds(0));\n\n    return ret_code;\n}\n\nvoid ReactiveFrontend::cleanup()\n{\n    _engine->enable_realtime(false);\n}\n\nvoid ReactiveFrontend::run()\n{\n    _engine->enable_realtime(true);\n}\n\n// TODO: While in JUCE plugins channel count can change, in sushi it's set on init.\n//  In JUCE, the buffer size is always the same for in and out, with some unused,\n//  if they differ.\nvoid ReactiveFrontend::process_audio(ChunkSampleBuffer& in_buffer,\n                                     ChunkSampleBuffer& out_buffer,\n                                     int64_t total_sample_count,\n                                     Time timestamp)\n{\n    // TODO: Do we need to concern ourselves with multiple buses?\n\n    // TODO: Deal also with MIDI.\n\n    // TODO: Deal also with CV.\n\n    out_buffer.clear();\n\n    if (_pause_manager.should_process())\n    {\n        _engine->process_chunk(&in_buffer,\n                               &out_buffer,\n                               &_in_controls,\n                               &_out_controls,\n                               timestamp,\n                               total_sample_count);\n\n        if (_pause_manager.should_ramp())\n        {\n            _pause_manager.ramp_output(out_buffer);\n        }\n    }\n}\n\nvoid ReactiveFrontend::notify_interrupted_audio(Time duration)\n{\n    _engine->notify_interrupted_audio(duration);\n}\n\n} // end namespace sushi::internal::audio_frontend"
  },
  {
    "path": "src/audio_frontends/reactive_frontend.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n * @brief Reactive frontend to process audio from a callback through a host application.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_REACTIVE_FRONTEND_H\n#define SUSHI_REACTIVE_FRONTEND_H\n\n#include <string>\n#include <vector>\n#include <atomic>\n#include <thread>\n\n#include <sndfile.h>\n\n#include \"base_audio_frontend.h\"\n#include \"library/rt_event.h\"\n\nnamespace sushi::internal::audio_frontend {\n\n// TODO: Hard-coding the number of channels for now.\nconstexpr int REACTIVE_FRONTEND_CHANNELS = 2;\n\nstruct ReactiveFrontendConfiguration : public BaseAudioFrontendConfiguration\n{\n    ReactiveFrontendConfiguration(int cv_inputs,\n                                  int cv_outputs) :\n            BaseAudioFrontendConfiguration(cv_inputs, cv_outputs)\n    {}\n\n    ~ReactiveFrontendConfiguration() override = default;\n};\n\nclass ReactiveFrontend : public BaseAudioFrontend\n{\npublic:\n    explicit ReactiveFrontend(engine::BaseEngine* engine) : BaseAudioFrontend(engine) {}\n\n    ~ReactiveFrontend() override\n    {\n        cleanup();\n    }\n\n    /**\n     * @brief Initialize frontend with the given configuration.\n     *        If anything can go wrong during initialization, partially allocated\n     *        resources should be freed by calling cleanup().\n     *\n     * @param config Should be an object of the proper derived configuration class.\n     * @return AudioFrontendInitStatus::OK in case of success,\n     *         or different error code otherwise.\n     */\n    AudioFrontendStatus init(BaseAudioFrontendConfiguration* config) override;\n\n    /**\n     * @brief Free resources allocated during init. stops the frontend if currently running.\n     */\n    void cleanup() override;\n\n    /**\n     * @brief Run engine main loop.\n     */\n    void run() override;\n\n    /**\n     * @brief Method to invoke from the host's audio callback.\n     * @param in_buffer Input sample buffer\n     * @param out_buffer Output sample buffer\n     * @param channel_count number of audio channels\n     * @param total_sample_count since start (timestamp)\n     * @param timestamp timestamp for call\n     */\n     void process_audio(ChunkSampleBuffer& in_buffer,\n                        ChunkSampleBuffer& out_buffer,\n                        int64_t total_sample_count,\n                        Time timestamp);\n\n     /**\n     * @brief Call before the first call to process_audio() when resuming from an interrupt or xrun to\n     *        notify sushi that audio processing was interrupted and that there may be gaps in the audio\n     * @param duration The length of the interruption\n     */\n     void notify_interrupted_audio(Time duration);\n\nprivate:\n    engine::ControlBuffer _in_controls;\n    engine::ControlBuffer _out_controls;\n};\n\n} // end namespace sushi::internal::audio_frontend\n\n#endif // SUSHI_REACTIVE_FRONTEND_H\n"
  },
  {
    "path": "src/audio_frontends/xenomai_raspa_frontend.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n  * @brief Frontend using Xenomai with RASPA library for XMOS board.\n  * @Copyright 2017-2023 Elk Audio AB, Stockholm\n  */\n\n#ifdef SUSHI_BUILD_WITH_RASPA\n\n#include <cerrno>\n\n#include <raspa/raspa.h>\n\n#include \"elklog/static_logger.h\"\n#include \"xenomai_raspa_frontend.h\"\n#include \"audio_frontend_internals.h\"\n\nnamespace sushi::internal::audio_frontend {\n\n/**\n * Ensure version compatibility with raspa library\n */\nconstexpr int REQUIRED_RASPA_VER_MAJ = 1;\nconstexpr int REQUIRED_RASPA_VER_MIN = 2;\nstatic_assert(REQUIRED_RASPA_VER_MAJ == RASPA_VERSION_MAJ, \"Raspa major version mismatch\");\nstatic_assert(REQUIRED_RASPA_VER_MIN == RASPA_VERSION_MIN, \"Raspa minor version mismatch\");\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"raspa audio\");\n\nbool XenomaiRaspaFrontend::_raspa_initialised = false;\n\nAudioFrontendStatus XenomaiRaspaFrontend::init(BaseAudioFrontendConfiguration* config)\n{\n    auto ret_code = BaseAudioFrontend::init(config);\n    if (ret_code != AudioFrontendStatus::OK)\n    {\n        return ret_code;\n    }\n\n    auto raspa_config = static_cast<const XenomaiRaspaFrontendConfiguration*>(_config);\n\n    unsigned int debug_flags = 0;\n    if (raspa_config->break_on_mode_sw)\n    {\n        debug_flags |= RASPA_DEBUG_SIGNAL_ON_MODE_SW;\n    }\n\n    auto raspa_ret = raspa_open(AUDIO_CHUNK_SIZE, rt_process_callback, this, debug_flags);\n    if (raspa_ret < 0)\n    {\n        ELKLOG_LOG_ERROR(\"Error opening RASPA: {}\", raspa_get_error_msg(-raspa_ret));\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    auto cv_audio_status = config_audio_channels(raspa_config);\n    if (cv_audio_status != AudioFrontendStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Incompatible cv and audio channel setup\");\n        return cv_audio_status;\n    }\n\n    auto raspa_sample_rate = raspa_get_sampling_rate();\n    if (raspa_sample_rate == 0)\n    {\n        ELKLOG_LOG_ERROR(\"Raspa has invalid sample rate of 0.\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    ELKLOG_LOG_WARNING_IF(_engine->sample_rate() != raspa_sample_rate,\n                          \"Sample rate mismatch between engine ({}) and Raspa ({}), setting to {}\",\n                          _engine->sample_rate(), raspa_sample_rate, raspa_sample_rate);\n\n    _set_engine_sample_rate(raspa_sample_rate);\n    _engine->set_output_latency(std::chrono::microseconds(raspa_get_output_latency()));\n\n    return AudioFrontendStatus::OK;\n}\n\nvoid XenomaiRaspaFrontend::cleanup()\n{\n    _engine->enable_realtime(false);\n    if (_raspa_initialised)\n    {\n        ELKLOG_LOG_INFO(\"Closing Raspa driver.\");\n        raspa_close();\n    }\n    _raspa_initialised = false;\n}\n\nvoid XenomaiRaspaFrontend::run()\n{\n    _engine->enable_realtime(true);\n    raspa_start_realtime();\n}\n\nint XenomaiRaspaFrontend::global_init()\n{\n    auto status = raspa_init();\n    _raspa_initialised = status == 0;\n    return status;\n}\n\nvoid XenomaiRaspaFrontend::_internal_process_callback(float* input, float* output)\n{\n    Time timestamp = Time(raspa_get_time());\n    set_flush_denormals_to_zero();\n    int64_t samplecount = raspa_get_samplecount();\n\n    // Gate in signals from the Sika board are inverted, hence invert all bits\n    _in_controls.gate_values = ~engine::BitSet32(raspa_get_gate_values());\n\n    ChunkSampleBuffer in_buffer = ChunkSampleBuffer::create_from_raw_pointer(input, 0, _audio_input_channels);\n    ChunkSampleBuffer out_buffer = ChunkSampleBuffer::create_from_raw_pointer(output, 0, _audio_output_channels);\n    for (int i = 0; i < _cv_input_channels; ++i)\n    {\n        _in_controls.cv_values[i] = map_audio_to_cv(input[(_audio_input_channels + i + 1) * AUDIO_CHUNK_SIZE - 1] * CV_IN_CORR);\n    }\n\n    out_buffer.clear();\n    _handle_resume(timestamp, AUDIO_CHUNK_SIZE);\n\n    if (_pause_manager.should_process())\n    {\n        _engine->process_chunk(&in_buffer, &out_buffer, &_in_controls, &_out_controls, timestamp, samplecount);\n        if (_pause_manager.should_ramp())\n        {\n            _pause_manager.ramp_output(out_buffer);\n        }\n       raspa_set_gate_values(static_cast<uint32_t>(_out_controls.gate_values.to_ulong()));\n        /* Sika board outputs only positive cv */\n        for (int i = 0; i < _cv_output_channels; ++i)\n        {\n            float* out_data = output + (_audio_output_channels + i) * AUDIO_CHUNK_SIZE;\n            _cv_output_hist[i] = ramp_cv_output(out_data, _cv_output_hist[i], _out_controls.cv_values[i] * CV_OUT_CORR);\n        }\n    }\n\n    _handle_pause(timestamp);\n}\n\nAudioFrontendStatus XenomaiRaspaFrontend::config_audio_channels(const XenomaiRaspaFrontendConfiguration* config)\n{\n    /* CV channels ar counted from the back, so if RASPA_N_CHANNELS is 8 and\n     * cv inputs is set to 2, The engine will be set to 6 audio input channels\n     * and the last 2 will be used as cv input 0 and cv input 1, respectively\n     * In the first revision Sika, CV outs are on channels 4 and 5 (counted from 0) and\n     * optional on 6 and 7, so only 0 or 4 cv channels is accepted */\n    if (config->cv_inputs != 0 && config->cv_inputs != 2)\n    {\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    if (config->cv_outputs != 0 && config->cv_outputs != 4)\n    {\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    auto num_total_input_channels = raspa_get_num_input_channels();\n    auto num_total_output_channels = raspa_get_num_output_channels();\n    if (num_total_input_channels == 0 && num_total_output_channels == 0)\n    {\n        ELKLOG_LOG_ERROR(\"RASPA has no input or output channels.\");\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n\n    _cv_input_channels = config->cv_inputs;\n    _cv_output_channels = config->cv_outputs;\n    _audio_input_channels = num_total_input_channels - _cv_input_channels;\n    _audio_output_channels = num_total_output_channels - _cv_output_channels;\n    _engine->set_audio_channels(_audio_input_channels, _audio_output_channels);\n    auto status = _engine->set_cv_input_channels(_cv_input_channels);\n    if (status != engine::EngineReturnStatus::OK)\n    {\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    status = _engine->set_cv_output_channels(_cv_output_channels);\n    if (status != engine::EngineReturnStatus::OK)\n    {\n        return AudioFrontendStatus::AUDIO_HW_ERROR;\n    }\n    return AudioFrontendStatus::OK;\n}\n\n} // end namespace sushi::internal::audio_frontend\n\n#else // SUSHI_BUILD_WITH_RASPA\n\n#include \"elklog/static_logger.h\"\n\n#include \"xenomai_raspa_frontend.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nELKLOG_GET_LOGGER;\n\nXenomaiRaspaFrontend::XenomaiRaspaFrontend(engine::BaseEngine* engine) : BaseAudioFrontend(engine)\n{}\n\nAudioFrontendStatus XenomaiRaspaFrontend::init(BaseAudioFrontendConfiguration*)\n{\n    /* The log print needs to be in a cpp file for initialisation order reasons */\n    ELKLOG_LOG_ERROR(\"Sushi was not built with Xenomai Cobalt support!\");\n    return AudioFrontendStatus::AUDIO_HW_ERROR;\n}\n\n}\n\n#endif // SUSHI_BUILD_WITH_RASPA\n"
  },
  {
    "path": "src/audio_frontends/xenomai_raspa_frontend.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Frontend using Xenomai rt framework\n *        and RTDM driver for audio over SPI\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_XENOMAI_RASPA_FRONTEND_H\n#define SUSHI_XENOMAI_RASPA_FRONTEND_H\n\n#ifdef SUSHI_BUILD_WITH_RASPA\n\n#include \"base_audio_frontend.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nstruct XenomaiRaspaFrontendConfiguration : public BaseAudioFrontendConfiguration\n{\n    XenomaiRaspaFrontendConfiguration(bool break_on_mode_sw,\n                                      int cv_inputs,\n                                      int cv_outputs) : BaseAudioFrontendConfiguration(cv_inputs, cv_outputs),\n                                                        break_on_mode_sw(break_on_mode_sw) {}\n\n    virtual ~XenomaiRaspaFrontendConfiguration() = default;\n    bool break_on_mode_sw;\n};\n\nclass XenomaiRaspaFrontend : public BaseAudioFrontend\n{\npublic:\n    explicit XenomaiRaspaFrontend(engine::BaseEngine* engine) : BaseAudioFrontend(engine) {}\n\n    virtual ~XenomaiRaspaFrontend()\n    {\n        cleanup();\n    }\n\n    /**\n     * @brief Initialize the frontend and the audio driver.\n     * @param config Configuration struct\n     * @return OK on successful initialization, error otherwise.\n     */\n    AudioFrontendStatus init(BaseAudioFrontendConfiguration* config) override;\n\n    /**\n     * @brief Static callback passed to RASPA\n     * @param input Input buffer in interleaved format\n     * @param output Output buffer in interleaved format\n     * @param data Opaque pointer to user data (this ptr in our case)\n     */\n    static void rt_process_callback(float* input, float* output, void* data)\n    {\n        return static_cast<XenomaiRaspaFrontend*>(data)->_internal_process_callback(input, output);\n    }\n\n    /**\n     * @brief Call to clean up resources and release ports\n     */\n    void cleanup() override;\n\n    /**\n     * @brief Activate the realtime frontend, currently blocking.\n     */\n    void run() override;\n\n    /**\n     * @brief Workaround for Xenomai process initialization, which should happen\n     *        as the _first_ thing in main() before everything else.\n     *\n     * @return 0 if successful, raspa_init() error code otherwise\n     */\n    static int global_init();\n\nprivate:\n    /* Internal process callback function */\n    void _internal_process_callback(float* input, float* output);\n\n    AudioFrontendStatus config_audio_channels(const XenomaiRaspaFrontendConfiguration* config);\n\n    static bool _raspa_initialised;\n    int _audio_input_channels;\n    int _audio_output_channels;\n    int _cv_input_channels;\n    int _cv_output_channels;\n    engine::ControlBuffer _in_controls;\n    engine::ControlBuffer _out_controls;\n    std::array<float, MAX_ENGINE_CV_IO_PORTS> _cv_output_hist{0};\n};\n\n} // end namespace sushi::internal::audio_frontend\n\n\n#else // SUSHI_BUILD_WITH_RASPA\n// Dummy frontend for non-Cobalt builds\n\n#include \"base_audio_frontend.h\"\n\nnamespace sushi::internal::audio_frontend {\n\nstruct XenomaiRaspaFrontendConfiguration : public BaseAudioFrontendConfiguration\n{\n    XenomaiRaspaFrontendConfiguration(bool, int, int) : BaseAudioFrontendConfiguration(0, 0) {}\n};\n\nclass XenomaiRaspaFrontend : public BaseAudioFrontend\n{\npublic:\n    XenomaiRaspaFrontend(engine::BaseEngine* engine);\n    AudioFrontendStatus init(BaseAudioFrontendConfiguration*) override;\n    void cleanup() override {}\n    void run() override {}\n    void pause([[maybe_unused]] bool enabled) override {}\n};\n\n} // end namespace sushi::internal::audio_frontend\n\n#endif // SUSHI_BUILD_WITH_RASPA\n#endif // SUSHI_XENOMAI_RASPA_FRONTEND_H\n"
  },
  {
    "path": "src/concrete_sushi.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Main entry point to Sushi\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"factories/reactive_factory_implementation.h\"\n\n#include \"concrete_sushi.h\"\n\n#include \"engine/audio_engine.h\"\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n#include \"sushi_rpc/grpc_server.h\"\n#endif\n\nnamespace sushi {\n\nstd::optional<std::pair<std::string, int>> SushiOptions::grpc_address_and_port()\n{\n    auto last_colon_index = grpc_listening_address.find_last_of(':');\n    if (last_colon_index == std::string::npos)\n    {\n        return std::nullopt;\n    }\n\n    last_colon_index++; // to include the last ':' in the address part.\n\n    auto address_part = grpc_listening_address.substr(0, last_colon_index);\n\n    int port = -1;\n    try\n    {\n        port = std::stoi(grpc_listening_address.substr(last_colon_index));\n    }\n    catch (...)\n    {\n        return std::nullopt;\n    }\n\n    return std::optional<std::pair<std::string, int>>({address_part, port});\n}\n\nbool SushiOptions::increment_grpc_port_number()\n{\n    auto address_and_port = grpc_address_and_port();\n\n    if (address_and_port.has_value())\n    {\n        grpc_listening_address = address_and_port->first +\n                                 std::to_string(address_and_port->second + 1);\n        return true;\n    }\n    else\n    {\n        return false;\n    }\n}\n\nstd::string to_string(Status status)\n{\n    switch (status)\n    {\n        case Status::FAILED_LOAD_HOST_CONFIG:\n            return \"Failed to load host configuration from config file.\";\n        case Status::FAILED_INVALID_CONFIGURATION_FILE:\n            return \"Error reading host config, check logs for details.\";\n        case Status::FAILED_LOAD_TRACKS:\n            return \"Failed to load tracks from the Json config file.\";\n        case Status::FAILED_LOAD_MIDI_MAPPING:\n            return \"Failed to load MIDI mapping from the Json config file.\";\n        case Status::FAILED_LOAD_CV_GATE:\n            return \"Failed to load CV and Gate configuration.\";\n        case Status::FAILED_LOAD_PROCESSOR_STATES:\n            return \"Failed to load the initial processor states.\";\n        case Status::FAILED_LOAD_EVENT_LIST:\n            return \"Failed to load Event list from the Json config file.\";\n        case Status::FAILED_LOAD_EVENTS:\n            return \"Failed to load Events from the Json config file.\";\n        case Status::FAILED_LOAD_OSC:\n            return \"Failed to load OSC echo specification from the Json config file.\";\n        case Status::FAILED_OSC_FRONTEND_INITIALIZATION:\n            return \"Failed to setup the OSC frontend.\";\n        case Status::FAILED_INVALID_FILE_PATH:\n            return \"Error reading config file, invalid file path: \";\n        case Status::FAILED_XENOMAI_INITIALIZATION:\n            return \"Failed to initialize the Xenomai process, err. code: \";\n        case Status::FAILED_AUDIO_FRONTEND_MISSING:\n            return \"No audio frontend is selected.\";\n        case Status::FAILED_AUDIO_FRONTEND_INITIALIZATION:\n            return \"Error initializing frontend, check logs for details.\";\n        case Status::FAILED_MIDI_FRONTEND_INITIALIZATION:\n            return \"Failed to setup the Midi frontend.\";\n        case Status::FAILED_TO_START_RPC_SERVER:\n            return \"Failed to start the RPC server.\";\n        case Status::SUSHI_ALREADY_STARTED:\n            return \"Sushi has already been started\";\n        case Status::SUSHI_THREW_EXCEPTION:\n            return \"Sushi has thrown an exception\";\n        case Status::OK:\n            return \"Ok\";\n        default:\n            assert(false);\n            return \"\";\n    }\n}\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"concrete_sushi\");\n\nnamespace internal {\n\n///////////////////////////////////////////\n// Sushi methods                         //\n///////////////////////////////////////////\n\nConcreteSushi::ConcreteSushi() = default;\n\nConcreteSushi::~ConcreteSushi()\n{\n    stop();\n}\n\nStatus ConcreteSushi::start()\n{\n    if (_osc_frontend != nullptr)\n    {\n        _osc_frontend->run();\n    }\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n    if (_rpc_server != nullptr)\n    {\n        bool rpc_server_status = _rpc_server->start();\n        if (!rpc_server_status)\n        {\n            if (_osc_frontend != nullptr)\n            {\n                _osc_frontend->stop();\n            }\n            return Status::FAILED_TO_START_RPC_SERVER;\n        }\n    }\n#endif\n\n    _audio_frontend->run();\n    _engine->event_dispatcher()->run();\n    _midi_frontend->run();\n\n    return Status::OK;\n}\n\nvoid ConcreteSushi::stop()\n{\n    ELKLOG_LOG_INFO(\"Stopping Sushi.\");\n\n    if (_audio_frontend != nullptr)\n    {\n        _audio_frontend->cleanup();\n        _audio_frontend.reset();\n    }\n\n    if (_engine != nullptr)\n    {\n        _engine->event_dispatcher()->stop();\n    }\n\n    if (_osc_frontend != nullptr)\n    {\n        _osc_frontend->stop();\n        _osc_frontend.reset();\n    }\n\n    if (_midi_frontend != nullptr)\n    {\n        _midi_frontend->stop();\n        _midi_frontend.reset();\n    }\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n    if (_rpc_server != nullptr)\n    {\n        _rpc_server->stop();\n        _rpc_server.reset();\n    }\n#endif\n}\n\ncontrol::SushiControl* ConcreteSushi::controller()\n{\n    return _engine_controller.get();\n}\n\nvoid ConcreteSushi::set_sample_rate(float sample_rate)\n{\n    _engine->set_sample_rate(sample_rate);\n}\n\nfloat ConcreteSushi::sample_rate() const\n{\n    return _engine->sample_rate();\n}\n\n} // end namespace internal\n\n} // end namespace sushi::internal\n"
  },
  {
    "path": "src/concrete_sushi.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Main entry point to Sushi\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SUSHI_H\n#define SUSHI_SUSHI_H\n\n#include <cassert>\n#include <memory>\n#include <chrono>\n#include <optional>\n\n#include \"sushi/sushi.h\"\n#include \"sushi/compile_time_settings.h\"\n\nnamespace sushi_rpc {\nclass GrpcServer;\n}\n\nnamespace sushi::internal {\n\nclass BaseFactory;\n\nnamespace engine {\nclass JsonConfigurator;\nclass AudioEngine;\nclass Controller;\n}\n\nnamespace audio_frontend {\nstruct BaseAudioFrontendConfiguration;\nclass BaseAudioFrontend;\nclass XenomaiRaspaFrontend;\nclass PortAudioFrontend;\nclass OfflineFrontend;\nclass JackFrontend;\nclass ReactiveFrontend;\n}\n\nnamespace midi_frontend {\nclass BaseMidiFrontend;\nclass ReactiveMidiFrontend;\n}\n\nnamespace midi_dispatcher {\nclass MidiDispatcher;\n}\n\nnamespace control_frontend {\nclass OSCFrontend;\n}\n\nnamespace jsonconfig {\nclass JsonConfigurator;\n}\n\nclass ConcreteSushiAccessor;\n\nclass ConcreteSushi : public Sushi\n{\npublic:\n    ~ConcreteSushi() override;\n\n    /**\n     * Given Sushi is initialized successfully, call this before the audio callback is first invoked.\n     */\n    Status start() override;\n\n    /**\n     * Stops the Sushi instance from running.\n     */\n    void stop() override;\n\n    /**\n     * @return an instance of the Sushi controller - assuming Sushi has first been initialized.\n     */\n    control::SushiControl* controller() override;\n\n    void set_sample_rate(float sample_rate) override;\n\n    [[nodiscard]] float sample_rate() const override;\n\nprotected:\n    /**\n     * @brief To create a Sushi instance, call _make_sushi(...) from inside a class inheriting from FactoryBase.\n     */\n    ConcreteSushi();\n\n    friend BaseFactory;\n    friend ConcreteSushiAccessor;\n\n    std::unique_ptr<engine::AudioEngine> _engine {nullptr};\n    std::unique_ptr<midi_dispatcher::MidiDispatcher> _midi_dispatcher {nullptr};\n\n    std::unique_ptr<midi_frontend::BaseMidiFrontend> _midi_frontend {nullptr};\n    std::unique_ptr<control_frontend::OSCFrontend> _osc_frontend {nullptr};\n    std::unique_ptr<audio_frontend::BaseAudioFrontend> _audio_frontend {nullptr};\n    std::unique_ptr<audio_frontend::BaseAudioFrontendConfiguration> _frontend_config {nullptr};\n\n    std::unique_ptr<engine::Controller> _engine_controller {nullptr};\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n    std::unique_ptr<sushi_rpc::GrpcServer> _rpc_server {nullptr};\n#endif\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_SUSHI_H\n"
  },
  {
    "path": "src/control_frontends/alsa_midi_frontend.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Alsa midi frontend\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <chrono>\n#include <cstdlib>\n\n#include <alsa/seq_event.h>\n\n#include \"elklog/static_logger.h\"\n\n#include \"alsa_midi_frontend.h\"\n#include \"library/midi_decoder.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"alsamidi\");\n\nnamespace sushi::internal::midi_frontend {\n\nconstexpr auto ALSA_POLL_TIMEOUT = std::chrono::milliseconds(200);\nconstexpr auto CLIENT_NAME = \"Sushi\";\n\nint create_port(snd_seq_t* seq, int queue, const std::string& name, bool is_input)\n{\n    unsigned int capabilities;\n    if (is_input)\n    {\n        capabilities = SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE;\n    }\n    else\n    {\n        capabilities = SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ;\n    }\n\n    int port = snd_seq_create_simple_port(seq, name.c_str(), capabilities, SND_SEQ_PORT_TYPE_APPLICATION);\n\n    if (port < 0)\n    {\n        ELKLOG_LOG_ERROR(\"Error opening ALSA MIDI port {}: {}\", name, strerror(-port));\n        return port;\n    }\n\n    /* For some weird reason directly creating the port with the specific timestamping\n     * doesn't work, but we can set them once the port has been created */\n    snd_seq_port_info_t* port_info;\n    snd_seq_port_info_alloca(&port_info);\n    snd_seq_get_port_info(seq, port, port_info);\n    snd_seq_port_info_set_timestamp_queue(port_info, queue);\n    snd_seq_port_info_set_timestamping(port_info, 1);\n    snd_seq_port_info_set_timestamp_real(port_info, 1);\n    int alsamidi_ret = snd_seq_set_port_info(seq, port, port_info);\n    if (alsamidi_ret < 0)\n    {\n        ELKLOG_LOG_ERROR(\"Couldn't set output port time configuration on port {}: {}\", name, strerror(-alsamidi_ret));\n        return alsamidi_ret;\n    }\n    ELKLOG_LOG_INFO(\"Created Alsa Midi port {}\", name);\n    return port;\n}\n\n/**\n * @brief Filters midi messages currently not handled by sushi\n * @param type Alsa event type enumeration (see alsa/seq_event.h)\n * @return true if the event type corresponds to a midi message that should be handled by sushi\n *              false otherwise.\n */\nbool is_midi_for_sushi(snd_seq_event_type_t type)\n{\n    if (type >= SND_SEQ_EVENT_NOTE && type <= SND_SEQ_EVENT_PITCHBEND)\n    {\n        return true;\n    }\n    return false;\n}\n\nAlsaMidiFrontend::AlsaMidiFrontend(int inputs, int outputs, midi_receiver::MidiReceiver* dispatcher)\n        : BaseMidiFrontend(dispatcher),\n          _inputs(inputs),\n          _outputs(outputs)\n{}\n\nAlsaMidiFrontend::~AlsaMidiFrontend()\n{\n    stop();\n    snd_midi_event_free(_input_parser);\n    snd_midi_event_free(_output_parser);\n    snd_seq_free_queue(_seq_handle, _queue);\n    snd_seq_close(_seq_handle);\n}\n\nbool AlsaMidiFrontend::init()\n{\n    auto alsamidi_ret = snd_seq_open(&_seq_handle, \"default\", SND_SEQ_OPEN_DUPLEX, 0);\n    if (alsamidi_ret < 0)\n    {\n        ELKLOG_LOG_ERROR(\"Error opening MIDI port: {}\", strerror(-alsamidi_ret));\n        return false;\n    }\n\n    alsamidi_ret = snd_seq_set_client_name(_seq_handle, CLIENT_NAME);\n    if (alsamidi_ret < 0)\n    {\n        ELKLOG_LOG_ERROR(\"Error setting client name: {}\", strerror(-alsamidi_ret));\n        return false;\n    }\n\n    _queue = snd_seq_alloc_queue(_seq_handle);\n\n    alsamidi_ret = snd_seq_start_queue(_seq_handle, _queue, nullptr);\n    if (alsamidi_ret < 0)\n    {\n        ELKLOG_LOG_ERROR(\"Error setting up event queue {}\", strerror(-alsamidi_ret));\n        return false;\n    }\n\n    if (_init_ports() == false)\n    {\n        return false;\n    }\n\n    alsamidi_ret = snd_midi_event_new(ALSA_EVENT_MAX_SIZE, &_input_parser);\n    if (alsamidi_ret < 0)\n    {\n        ELKLOG_LOG_ERROR(\"Error creating MIDI Input RtEvent Parser: {}\", strerror(-alsamidi_ret));\n        return false;\n    }\n    alsamidi_ret = snd_midi_event_new(ALSA_EVENT_MAX_SIZE, &_output_parser);\n    if (alsamidi_ret < 0)\n    {\n        ELKLOG_LOG_ERROR(\"Error creating MIDI Output RtEvent Parser: {}\", strerror(-alsamidi_ret));\n        return false;\n    }\n    alsamidi_ret = snd_seq_nonblock(_seq_handle, 1);\n    if (alsamidi_ret < 0)\n    {\n        ELKLOG_LOG_ERROR(\"Setting non-blocking mode failed: {}\", strerror(-alsamidi_ret));\n        return false;\n    }\n\n    snd_midi_event_no_status(_input_parser, 1); /* Disable running status in the decoder */\n    snd_midi_event_no_status(_output_parser, 1); /* Disable running status in the encoder */\n\n    if (alsamidi_ret < 0 )\n    {\n        ELKLOG_LOG_ERROR(\"Failed to set sequencer to use queue: {}\", strerror(-alsamidi_ret));\n        return false;\n    }\n\n    snd_seq_drain_output(_seq_handle);\n\n    return _init_time();\n}\n\nvoid AlsaMidiFrontend::run()\n{\n    assert(_seq_handle);\n    if (_inputs > 0)\n    {\n        _running = true;\n        _worker = std::thread(&AlsaMidiFrontend::_poll_function, this);\n    }\n    ELKLOG_LOG_INFO_IF(_inputs == 0, \"No of midi inputs is 0, not starting read thread\")\n}\n\nvoid AlsaMidiFrontend::stop()\n{\n    _running.store(false);\n    if (_worker.joinable())\n    {\n        _worker.join();\n    }\n}\n\nvoid AlsaMidiFrontend::_poll_function()\n{\n    auto descr_count = static_cast<nfds_t>(snd_seq_poll_descriptors_count(_seq_handle, POLLIN));\n    auto descriptors = std::make_unique<pollfd[]>(descr_count);\n    snd_seq_poll_descriptors(_seq_handle, descriptors.get(), static_cast<unsigned int>(descr_count), POLLIN);\n    while (_running)\n    {\n        if (poll(descriptors.get(), descr_count, ALSA_POLL_TIMEOUT.count()) > 0)\n        {\n            snd_seq_event_t* ev{nullptr};\n            uint8_t data_buffer[ALSA_EVENT_MAX_SIZE]{0};\n            while (snd_seq_event_input(_seq_handle, &ev) > 0)\n            {\n                if (is_midi_for_sushi(ev->type))\n                {\n                    auto byte_count = snd_midi_event_decode(_input_parser, data_buffer, sizeof(data_buffer), ev);\n                    if (byte_count > 0)\n                    {\n                        auto input = _port_to_input_map.find(ev->dest.port);\n                        if (input != _port_to_input_map.end())\n                        {\n                            bool timestamped = (ev->flags | (SND_SEQ_TIME_STAMP_REAL & SND_SEQ_TIME_MODE_ABS)) == 1;\n                            Time timestamp = timestamped ? _to_sushi_time(&ev->time.time) : IMMEDIATE_PROCESS;\n                            _receiver->send_midi(input->second, midi::to_midi_data_byte(data_buffer, byte_count), timestamp);\n\n                            ELKLOG_LOG_DEBUG(\"Received midi message: [{:x} {:x} {:x} {:x}], port{}, timestamp: {}\",\n                                            data_buffer[0], data_buffer[1], data_buffer[2], data_buffer[3], input->second, timestamp.count());\n\n                        }\n                    }\n                    ELKLOG_LOG_WARNING_IF(byte_count < 0, \"Decoder returned {}\", strerror(-byte_count))\n                }\n                snd_seq_free_event(ev);\n            }\n        }\n    }\n}\n\nvoid AlsaMidiFrontend::send_midi(int output, MidiDataByte data, [[maybe_unused]]Time timestamp)\n{\n    snd_seq_event ev;\n    snd_seq_ev_clear(&ev);\n    [[maybe_unused]] auto bytes = snd_midi_event_encode(_output_parser, data.data(), data.size(), &ev);\n\n    ELKLOG_LOG_INFO_IF(bytes <= 0, \"Failed to encode event: {} {}\", strerror(-bytes), ev.type)\n\n    snd_seq_ev_set_source(&ev, _output_midi_ports[output]);\n    snd_seq_ev_set_subs(&ev);\n    snd_seq_real_time_t ev_time = {0,0}; //_to_alsa_time(timestamp); TODO: Find a proper solution for the midi sync.\n    snd_seq_ev_schedule_real(&ev, _queue, false, &ev_time);\n    bytes = snd_seq_event_output(_seq_handle, &ev);\n    snd_seq_drain_output(_seq_handle);\n\n    ELKLOG_LOG_WARNING_IF(bytes <= 0, \"Event output returned: {}, type {}\", strerror(-bytes), ev.type)\n}\n\nbool AlsaMidiFrontend::_init_time()\n{\n    const snd_seq_real_time_t* start_time;\n    snd_seq_queue_status_t* queue_status;\n    snd_seq_queue_status_alloca(&queue_status);\n\n    int alsamidi_ret = snd_seq_get_queue_status(_seq_handle, _queue, queue_status);\n    if (alsamidi_ret < 0)\n    {\n        ELKLOG_LOG_ERROR(\"Couldn't get queue status {}\", strerror(-alsamidi_ret));\n        return false;\n    }\n    start_time = snd_seq_queue_status_get_real_time(queue_status);\n    _time_offset = get_current_time() - std::chrono::duration_cast<Time>(std::chrono::seconds(start_time->tv_sec) +\n                                                                         std::chrono::nanoseconds(start_time->tv_nsec));\n    return true;\n}\n\nbool AlsaMidiFrontend::_init_ports()\n{\n    bool add_index = _inputs > 1;\n    for (int i = 0; i < _inputs; ++i)\n    {\n        int port = create_port(_seq_handle, _queue, \"listen:in\" + (add_index? \"_\" + std::to_string(i + 1) : \"\"), true);\n        if (port < 0)\n        {\n            return false;\n        }\n        _input_midi_ports.push_back(port);\n        _port_to_input_map[port] = i;\n    }\n\n    add_index = _outputs > 1;\n    for (int i = 0; i < _outputs; ++i)\n    {\n        int port = create_port(_seq_handle, _queue, \"read:out\" + (add_index? \"_\" + std::to_string(i + 1) : \"\"), false);\n        if (port < 0)\n        {\n            return false;\n        }\n        _output_midi_ports.push_back(port);\n    }\n\n    return true;\n}\n\nTime AlsaMidiFrontend::_to_sushi_time(const snd_seq_real_time_t* alsa_time)\n{\n    return std::chrono::duration_cast<Time>(std::chrono::seconds(alsa_time->tv_sec) +\n                                            std::chrono::nanoseconds(alsa_time->tv_nsec)) + _time_offset;\n}\n\nsnd_seq_real_time_t AlsaMidiFrontend::_to_alsa_time(Time timestamp)\n{\n    snd_seq_real_time alsa_time;\n    auto offset_time =  timestamp - _time_offset;\n    auto seconds = std::chrono::duration_cast<std::chrono::seconds>(offset_time);\n    alsa_time.tv_sec = static_cast<unsigned int>(seconds.count());\n    alsa_time.tv_nsec = static_cast<unsigned int>(std::chrono::duration_cast<std::chrono::nanoseconds>(offset_time - seconds).count());\n    return alsa_time;\n}\n\n} // end namespace sushi::internal::midi_frontend\n\n"
  },
  {
    "path": "src/control_frontends/alsa_midi_frontend.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n * @brief Alsa midi frontend, provides a frontend for getting midi messages into the engine\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_ALSA_MIDI_FRONTEND_H\n#define SUSHI_ALSA_MIDI_FRONTEND_H\n#ifdef SUSHI_BUILD_WITH_ALSA_MIDI\n\n#include <thread>\n#include <atomic>\n#include <vector>\n#include <map>\n\n#include \"sushi/sushi_time.h\"\n\n#include <alsa/asoundlib.h>\n\n#include \"base_midi_frontend.h\"\n\nnamespace sushi::internal::midi_frontend {\n\nconstexpr int ALSA_EVENT_MAX_SIZE = 12;\n\nclass AlsaMidiFrontend : public BaseMidiFrontend\n{\npublic:\n    AlsaMidiFrontend(int inputs, int outputs, midi_receiver::MidiReceiver* dispatcher);\n\n    ~AlsaMidiFrontend() override;\n\n    bool init() override;\n\n    void run() override;\n\n    void stop() override;\n\n    void send_midi(int input, MidiDataByte data, [[maybe_unused]]Time timestamp) override;\n\nprivate:\n    bool _init_ports();\n    bool _init_time();\n    Time _to_sushi_time(const snd_seq_real_time_t* alsa_time);\n    snd_seq_real_time_t _to_alsa_time(Time timestamp);\n\n    void                        _poll_function();\n    std::thread                 _worker;\n    std::atomic<bool>           _running{false};\n    snd_seq_t*                  _seq_handle{nullptr};\n    int                         _inputs;\n    int                         _outputs;\n    std::vector<int>            _input_midi_ports;\n    std::vector<int>            _output_midi_ports;\n    std::map<int, int>          _port_to_input_map;\n    int                         _queue{-1};\n\n    snd_midi_event_t*           _input_parser{nullptr};\n    snd_midi_event_t*           _output_parser{nullptr};\n    Time                        _time_offset{0};\n};\n\n} // end namespace sushi::internal::midi_frontend\n\n#endif // SUSHI_BUILD_WITH_ALSA_MIDI\n\n#endif // SUSHI_ALSA_MIDI_FRONTEND_H_H\n"
  },
  {
    "path": "src/control_frontends/base_control_frontend.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Base class for control frontend\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"library/midi_encoder.h\"\n#include \"control_frontends/base_control_frontend.h\"\n\nnamespace sushi::internal::control_frontend {\n\nvoid BaseControlFrontend::send_parameter_change_event(ObjectId processor,\n                                                      ObjectId parameter,\n                                                      float value)\n{\n    Time timestamp = IMMEDIATE_PROCESS;\n    _event_dispatcher->post_event(std::make_unique<ParameterChangeEvent>(ParameterChangeEvent::Subtype::FLOAT_PARAMETER_CHANGE,\n                                                                         processor, parameter, value, timestamp));\n}\n\nvoid BaseControlFrontend::send_string_parameter_change_event(ObjectId processor,\n                                                             ObjectId parameter,\n                                                             const std::string& value)\n{\n    Time timestamp = IMMEDIATE_PROCESS;\n    _event_dispatcher->post_event(std::make_unique<PropertyChangeEvent>(processor, parameter, value, timestamp));\n}\n\n\nvoid BaseControlFrontend::send_keyboard_event(ObjectId processor,\n                                              KeyboardEvent::Subtype type,\n                                              int channel,\n                                              int note,\n                                              float velocity)\n{\n    Time timestamp = IMMEDIATE_PROCESS;\n    _event_dispatcher->post_event(std::make_unique<KeyboardEvent>(type, processor, channel, note, velocity, timestamp));\n}\n\nvoid BaseControlFrontend::send_note_on_event(ObjectId processor, int channel, int note, float velocity)\n{\n    send_keyboard_event(processor, KeyboardEvent::Subtype::NOTE_ON, channel, note, velocity);\n}\n\nvoid BaseControlFrontend::send_note_off_event(ObjectId processor, int channel, int note, float velocity)\n{\n    send_keyboard_event(processor, KeyboardEvent::Subtype::NOTE_OFF, channel, note, velocity);\n}\n\nvoid BaseControlFrontend::send_program_change_event(ObjectId processor, int program)\n{\n    Time timestamp = IMMEDIATE_PROCESS;\n    _event_dispatcher->post_event(std::make_unique<ProgramChangeEvent>(processor, program, timestamp));\n}\n\nvoid BaseControlFrontend::send_set_tempo_event(float tempo)\n{\n    _event_dispatcher->post_event(std::make_unique<SetEngineTempoEvent>(tempo, IMMEDIATE_PROCESS));\n}\n\nvoid BaseControlFrontend::send_set_time_signature_event(TimeSignature signature)\n{\n    _event_dispatcher->post_event(std::make_unique<SetEngineTimeSignatureEvent>(signature, IMMEDIATE_PROCESS));\n}\n\nvoid BaseControlFrontend::send_set_playing_mode_event(PlayingMode mode)\n{\n    _event_dispatcher->post_event(std::make_unique<SetEnginePlayingModeStateEvent>(mode, IMMEDIATE_PROCESS));\n}\n\nvoid BaseControlFrontend::send_set_sync_mode_event(SyncMode mode)\n{\n    _event_dispatcher->post_event(std::make_unique<SetEngineSyncModeEvent>(mode, IMMEDIATE_PROCESS));\n}\n\nvoid BaseControlFrontend::send_with_callback(std::unique_ptr<Event> event)\n{\n    event->set_completion_cb(BaseControlFrontend::completion_callback, this);\n    _event_dispatcher->post_event(std::move(event));\n}\n\n} // end namespace sushi::internal::control_frontend\n"
  },
  {
    "path": "src/control_frontends/base_control_frontend.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n * @brief Base class for control frontend\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n * This module provides run-time control of the audio engine for parameter\n * changes and plugin control.\n */\n#ifndef SUSHI_BASE_CONTROL_FRONTEND_H\n#define SUSHI_BASE_CONTROL_FRONTEND_H\n\n#include <string>\n\n#include \"library/event_interface.h\"\n#include \"engine/base_engine.h\"\n\nnamespace sushi::internal::control_frontend {\n\nenum class ControlFrontendStatus\n{\n    OK,\n    ERROR,\n    INTERFACE_UNAVAILABLE\n};\n\nclass BaseControlFrontend : public EventPoster\n{\npublic:\n    explicit BaseControlFrontend(engine::BaseEngine* engine) : _engine(engine)\n    {\n        _event_dispatcher = _engine->event_dispatcher();\n    }\n\n    ~BaseControlFrontend() override = default;\n\n    static void completion_callback(void *arg, Event* event, int return_status)\n    {\n        static_cast<BaseControlFrontend*>(arg)->_completion_callback(event, return_status);\n    }\n\n    virtual ControlFrontendStatus init() = 0;\n\n    virtual void run() = 0;\n\n    virtual void stop() = 0;\n\n    void send_parameter_change_event(ObjectId processor, ObjectId parameter, float value);\n\n    void send_string_parameter_change_event(ObjectId processor, ObjectId parameter, const std::string& value);\n\n    void send_keyboard_event(ObjectId processor, KeyboardEvent::Subtype type, int channel, int note, float velocity);\n\n    void send_note_on_event(ObjectId processor, int channel, int note, float velocity);\n\n    void send_note_off_event(ObjectId processor, int channel, int note, float velocity);\n\n    void send_program_change_event(ObjectId processor, int program);\n\n    void send_set_tempo_event(float tempo);\n\n    void send_set_time_signature_event(TimeSignature signature);\n\n    void send_set_playing_mode_event(PlayingMode mode);\n\n    void send_set_sync_mode_event(SyncMode mode);\n\nprotected:\n    virtual void _completion_callback(Event* event, int return_status) = 0;\n\n    void send_with_callback(std::unique_ptr<Event> event);\n\n    engine::BaseEngine* _engine {nullptr};\n    dispatcher::BaseEventDispatcher* _event_dispatcher;\n\n};\n\n} // end namespace sushi::internal::control_frontend\n\n#endif // SUSHI_BASE_CONTROL_FRONTEND_H\n"
  },
  {
    "path": "src/control_frontends/base_midi_frontend.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n * @brief Base class for midi frontend\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n *\n * This module provides a frontend for getting midi messages into the engine\n */\n\n#ifndef SUSHI_BASE_MIDI_FRONTEND_H\n#define SUSHI_BASE_MIDI_FRONTEND_H\n\n#include \"sushi/types.h\"\n\n#include \"engine/midi_receiver.h\"\n\nnamespace sushi::internal::midi_frontend {\n\nclass BaseMidiFrontend\n{\npublic:\n    explicit BaseMidiFrontend(midi_receiver::MidiReceiver* receiver) : _receiver(receiver) {}\n\n    virtual ~BaseMidiFrontend() = default;\n\n    virtual bool init() = 0;\n\n    virtual void run() = 0;\n\n    virtual void stop() = 0;\n\n    virtual void send_midi(int input, MidiDataByte data, Time timestamp) = 0;\n\nprotected:\n    midi_receiver::MidiReceiver* _receiver;\n};\n\n/**\n * @brief A no-op implementation of the MidiFrontend. Simply discards all midi inputs sent to it.\n *        Useful for dummy and offline audio frontends.\n */\nclass NullMidiFrontend : public BaseMidiFrontend\n{\npublic:\n    explicit NullMidiFrontend(midi_receiver::MidiReceiver* receiver) : BaseMidiFrontend(receiver) {}\n    explicit NullMidiFrontend(int /* input */, int /* outputs */, midi_receiver::MidiReceiver* receiver) : BaseMidiFrontend(receiver) {}\n\n    bool init() override {return true;};\n\n    void run() override {};\n\n    void stop() override {};\n\n    void send_midi(int /*input*/, MidiDataByte /*data*/, Time /*timestamp*/) override {};\n};\n\n} // end namespace sushi::internal::midi_frontend\n\n#endif // SUSHI_BASE_MIDI_FRONTEND_H\n"
  },
  {
    "path": "src/control_frontends/osc_frontend.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief OSC runtime control frontend\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <algorithm>\n\n#include \"elklog/static_logger.h\"\n\n#include \"osc_utils.h\"\n#include \"osc_frontend.h\"\n\nnamespace sushi::internal {\n\nstd::string osc::make_safe_path(std::string name)\n{\n    // Based on which characters are invalid in the OSC Spec plus \\ and \"\n    constexpr std::string_view INVALID_CHARS = \"#*./?[]{}\\\"\\\\\";\n    for (char i : INVALID_CHARS)\n    {\n        name.erase(std::remove(name.begin(), name.end(), i), name.end());\n    }\n    std::replace(name.begin(), name.end(), ' ', '_');\n    return name;\n}\n\nnamespace control_frontend {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"osc frontend\");\n\nbool OscState::auto_enable_outputs() const\n{\n    return _auto_enable_outputs;\n}\n\nvoid OscState::set_auto_enable_outputs(bool value)\n{\n    _auto_enable_outputs = value;\n}\n\nconst std::vector<std::pair<std::string, std::vector<ObjectId>>>& OscState::enabled_outputs() const\n{\n    return _enabled_outputs;\n}\n\nvoid OscState::add_enabled_outputs(const std::string& processor_name,\n                                   const std::vector<ObjectId>& enabled_parameters)\n{\n    _enabled_outputs.emplace_back(processor_name, enabled_parameters);\n}\n\nvoid OscState::add_enabled_outputs(std::string&& processor_name,\n                                   std::vector<ObjectId>&& enabled_parameters)\n{\n    _enabled_outputs.emplace_back(processor_name, enabled_parameters);\n}\n\nOSCFrontend::OSCFrontend(engine::BaseEngine* engine,\n                         control::SushiControl* controller,\n                         osc::BaseOscMessenger* osc_interface) : BaseControlFrontend(engine),\n                                          _controller(controller),\n                                          _graph_controller(controller->audio_graph_controller()),\n                                          _param_controller(controller->parameter_controller()),\n                                          _processor_container(_engine->processor_container()),\n                                          _osc(osc_interface)\n{}\n\nOSCFrontend::~OSCFrontend()\n{\n    if (_running)\n    {\n        _stop_server();\n    }\n\n    if (_osc_initialized) // These are set up in init, where also _osc_initialized is set to true.\n    {\n        _osc->delete_method(_set_tempo_cp);\n        _osc->delete_method(_set_time_signature_cb);\n        _osc->delete_method(_set_playing_mode_cb);\n        _osc->delete_method(_set_sync_mode_cb);\n        _osc->delete_method(_set_timing_statistics_enabled_cb);\n        _osc->delete_method(_reset_timing_statistics_s_cb);\n        _osc->delete_method(_reset_timing_statistics_ss_cb);\n\n        _event_dispatcher->unsubscribe_from_parameter_change_notifications(this);\n        _event_dispatcher->unsubscribe_from_engine_notifications(this);\n        _osc_initialized = false;\n    }\n}\n\nControlFrontendStatus OSCFrontend::init()\n{\n    bool status = _osc->init();\n\n    if (status == false)\n    {\n        return ControlFrontendStatus::INTERFACE_UNAVAILABLE;\n    }\n\n    _setup_engine_control();\n    _osc_initialized = true;\n    _event_dispatcher->subscribe_to_parameter_change_notifications(this);\n    _event_dispatcher->subscribe_to_engine_notifications(this);\n\n    return ControlFrontendStatus::OK;\n}\n\nbool OSCFrontend::connect_from_parameter(const std::string& processor_name, const std::string& parameter_name)\n{\n    auto processor = _processor_container->processor(processor_name);\n    if (processor)\n    {\n        auto parameter = processor->parameter_from_name(parameter_name);\n        if (parameter)\n        {\n            _connect_from_parameter(processor_name, parameter_name, processor->id(), parameter->id());\n            return true;\n        }\n    }\n    return false;\n}\n\nbool OSCFrontend::disconnect_from_parameter(const std::string& processor_name, const std::string& parameter_name)\n{\n    auto processor = _processor_container->processor(processor_name);\n    if (processor)\n    {\n        auto parameter = processor->parameter_from_name(parameter_name);\n        if (parameter)\n        {\n            _outgoing_connections[processor->id()].erase(parameter->id());\n            return true;\n        }\n    }\n    return false;\n}\n\nbool OSCFrontend::connect_from_processor_parameters(const std::string& processor_name)\n{\n    auto processor = _processor_container->processor(processor_name);\n    if (processor)\n    {\n        for (auto& param: processor->all_parameters())\n        {\n            auto type = param->type();\n            if (type == ParameterType::FLOAT || type == ParameterType::INT || type == ParameterType::BOOL)\n            {\n                _connect_from_parameter(processor_name, param->name(), processor->id(), param->id());\n            }\n        }\n    }\n    return true;\n}\n\nvoid OSCFrontend::connect_from_all_parameters()\n{\n    auto processors = _processor_container->all_processors();\n    for (auto& processor : processors)\n    {\n        connect_from_processor_parameters(processor->name());\n    }\n}\n\nvoid OSCFrontend::disconnect_from_all_parameters()\n{\n    _outgoing_connections.clear();\n}\n\nstd::vector<std::string> OSCFrontend::get_enabled_parameter_outputs()\n{\n    auto outputs = std::vector<std::string>();\n\n    for (const auto& connection_pair : _outgoing_connections)\n    {\n        for (const auto& connection : connection_pair.second)\n        {\n            outputs.push_back(connection.second);\n        }\n    }\n\n    return outputs;\n}\n\nint OSCFrontend::process(Event* event)\n{\n    assert(_osc_initialized);\n\n    if (event->is_parameter_change_notification())\n    {\n        _handle_param_change_notification(static_cast<ParameterChangeNotificationEvent*>(event));\n    }\n    else if (event->is_property_change_notification())\n    {\n        _handle_property_change_notification(static_cast<PropertyChangeNotificationEvent*>(event));\n    }\n    else if (event->is_engine_notification())\n    {\n        _handle_engine_notification(static_cast<EngineNotificationEvent*>(event));\n    }\n    // Return statuses for notifications are not handled, so just return ok.\n    return EventStatus::HANDLED_OK;\n}\n\nstd::string OSCFrontend::send_ip() const\n{\n    return _osc->send_ip();\n}\n\nint OSCFrontend::send_port() const\n{\n    return _osc->send_port();\n}\n\nint OSCFrontend::receive_port() const\n{\n    return _osc->receive_port();\n}\n\nOscState OSCFrontend::save_state() const\n{\n    OscState state;\n    state.set_auto_enable_outputs(_connect_from_all_parameters);\n\n    /* Only the outgoing connections are saved as those can be configured manually,\n     * incoming osc messages are always connected to all parameters of all processors */\n    for (const auto& connection_pair : _outgoing_connections)\n    {\n        std::vector<ObjectId> enabled_params;\n        for (const auto& connection : connection_pair.second)\n        {\n            enabled_params.push_back(connection.first);\n        }\n        if (enabled_params.size() > 0)\n        {\n            auto processor = _processor_container->processor(connection_pair.first);\n            if (processor)\n            {\n                state.add_enabled_outputs(std::string(processor->name()), std::move(enabled_params));\n            }\n            ELKLOG_LOG_ERROR_IF(!processor, \"Processor {}, was not found when saving state\", connection_pair.first);\n        }\n    }\n\n    return state;\n}\n\nvoid OSCFrontend::set_state(const OscState& state)\n{\n    _outgoing_connections.clear();\n    _skip_outputs.clear();\n\n    _connect_from_all_parameters = state.auto_enable_outputs();\n\n    for (auto connections : state.enabled_outputs())\n    {\n        auto processor = _processor_container->processor(connections.first);\n        if (!processor)\n        {\n            ELKLOG_LOG_ERROR(\"Processor {} not found when restoring outgoing connections from state\", connections.first);\n            continue;\n        }\n        for (auto param_id : connections.second)\n        {\n            auto param_info = processor->parameter_from_id(param_id);\n            if (param_info)\n            {\n                _connect_from_parameter(connections.first, param_info->name(), processor->id(), param_id);\n            }\n        }\n        if (_connect_from_all_parameters)\n        {\n            /* This is so that when we later receive an asynchronous PROCESSOR_ADDED event, we\n             * should not add all parameter from this plugin. */\n            _skip_outputs[processor->id()] = true;\n        }\n    }\n}\n\nvoid OSCFrontend::_connect_from_parameter(const std::string& processor_name,\n                                          const std::string& parameter_name,\n                                          ObjectId processor_id,\n                                          ObjectId parameter_id)\n{\n    std::string id_string = \"/parameter/\" + osc::make_safe_path(processor_name) + \"/\" + osc::make_safe_path(parameter_name);\n\n    _outgoing_connections[processor_id][parameter_id] = id_string;\n\n    ELKLOG_LOG_DEBUG(\"Added osc output from parameter {}/{}\", processor_name, parameter_name);\n}\n\n\n\n\nstd::pair<OscConnection*, std::string> OSCFrontend::_create_processor_connection(const std::string& processor_name,\n                                                                                 ObjectId processor_id,\n                                                                                 const std::string& osc_path_prefix)\n{\n    std::string osc_path = osc_path_prefix + osc::make_safe_path(processor_name);\n    auto connection = new OscConnection;\n    connection->processor = processor_id;\n    connection->parameter = 0;\n    connection->instance = this;\n    connection->controller = _controller;\n    return {connection, osc_path};\n}\n\n\nOscConnection* OSCFrontend::_connect_kb_to_track(const Processor* processor)\n{\n    assert(_osc_initialized);\n    if (_osc_initialized == false)\n    {\n        return nullptr;\n    }\n\n    auto [connection, osc_path] = _create_processor_connection(processor->name(), processor->id(), \"/keyboard_event/\");\n    if (connection == nullptr)\n    {\n        return nullptr;\n    }\n\n    auto cb = _osc->add_method(osc_path.c_str(), \"siif\", osc::OscMethodType::SEND_KEYBOARD_NOTE_EVENT, connection);\n    connection->callback = cb;\n    _connections.push_back(std::unique_ptr<OscConnection>(connection));\n\n    auto dupl_conn = new OscConnection(*connection);\n    cb = _osc->add_method(osc_path.c_str(), \"sif\", osc::OscMethodType::SEND_KEYBOARD_MODULATION_EVENT, connection);\n    dupl_conn->callback = cb;\n    _connections.push_back(std::unique_ptr<OscConnection>(dupl_conn));\n    ELKLOG_LOG_DEBUG(\"Added osc callback {}\", osc_path);\n\n    return connection;\n}\n\nOscConnection* OSCFrontend::_connect_to_bypass_state(const Processor* processor)\n{\n    assert(_osc_initialized);\n    if (_osc_initialized == false)\n    {\n        return nullptr;\n    }\n\n    auto [connection, osc_path] = _create_processor_connection(processor->name(), processor->id(), \"/bypass/\");\n    if (connection == nullptr)\n    {\n        return nullptr;\n    }\n\n    auto cb = _osc->add_method(osc_path.c_str(), \"i\", osc::OscMethodType::SEND_BYPASS_STATE_EVENT, connection);\n    connection->callback = cb;\n    _connections.push_back(std::unique_ptr<OscConnection>(connection));\n    ELKLOG_LOG_DEBUG(\"Added osc callback {}\", osc_path);\n    return connection;\n}\n\nOscConnection* OSCFrontend::_connect_to_program_change(const Processor* processor)\n{\n    assert(_osc_initialized);\n    if (_osc_initialized == false)\n    {\n        return nullptr;\n    }\n\n    auto [connection, osc_path] = _create_processor_connection(processor->name(), processor->id(), \"/program/\");\n    if (connection == nullptr)\n    {\n        return nullptr;\n    }\n    auto cb = _osc->add_method(osc_path.c_str(), \"i\", osc::OscMethodType::SEND_PROGRAM_CHANGE_EVENT, connection);\n    connection->callback = cb;\n    _connections.push_back(std::unique_ptr<OscConnection>(connection));\n    ELKLOG_LOG_DEBUG(\"Added osc callback {}\", osc_path);\n    return connection;\n}\n\nOscConnection* OSCFrontend::_connect_to_parameter(const std::string& processor_name,\n                                                  const std::string& parameter_name,\n                                                  ObjectId processor_id,\n                                                  ObjectId parameter_id)\n{\n    assert(_osc_initialized);\n    if (_osc_initialized == false)\n    {\n        return nullptr;\n    }\n\n    std::string osc_path = \"/parameter/\" + osc::make_safe_path(processor_name) + \"/\" + osc::make_safe_path(parameter_name);\n    auto connection = new OscConnection;\n    connection->processor = processor_id;\n    connection->parameter = parameter_id;\n    connection->instance = this;\n    connection->controller = _controller;\n\n    auto cb = _osc->add_method(osc_path.c_str(), \"f\", osc::OscMethodType::SEND_PARAMETER_CHANGE_EVENT, connection);\n    connection->callback = cb;\n    _connections.push_back(std::unique_ptr<OscConnection>(connection));\n    ELKLOG_LOG_DEBUG(\"Added osc callback {}\", osc_path);\n\n    return connection;\n}\n\nOscConnection* OSCFrontend::_connect_to_property(const std::string& processor_name,\n                                                 const std::string& property_name,\n                                                 ObjectId processor_id,\n                                                 ObjectId property_id)\n{\n    assert(_osc_initialized);\n    if (_osc_initialized == false)\n    {\n        return nullptr;\n    }\n\n    std::string osc_path = \"/property/\" + osc::make_safe_path(processor_name) + \"/\" + osc::make_safe_path(property_name);\n    auto connection = new OscConnection;\n    connection->processor = processor_id;\n    connection->parameter = property_id;\n    connection->instance = this;\n    connection->controller = _controller;\n\n    auto cb = _osc->add_method(osc_path.c_str(), \"s\", osc::OscMethodType::SEND_PROPERTY_CHANGE_EVENT, connection);\n    connection->callback = cb;\n    _connections.push_back(std::unique_ptr<OscConnection>(connection));\n    ELKLOG_LOG_DEBUG(\"Added osc callback {}\", osc_path);\n\n    return connection;\n}\n\nvoid OSCFrontend::_connect_from_property(const std::string& processor_name,\n                                         const std::string& property_name,\n                                         ObjectId processor_id,\n                                         ObjectId property_id)\n{\n    std::string id_string = \"/property/\" + osc::make_safe_path(processor_name) + \"/\" + osc::make_safe_path(property_name);\n\n    _outgoing_connections[processor_id][property_id] = id_string;\n\n    ELKLOG_LOG_DEBUG(\"Added osc output from property {}/{}\", processor_name, property_name);\n}\n\nvoid OSCFrontend::_connect_to_parameters_and_properties(const Processor* processor)\n{\n    auto parameters = processor->all_parameters();\n    for (auto& param : parameters)\n    {\n        auto type = param->type();\n        if (type == ParameterType::FLOAT || type == ParameterType::INT || type == ParameterType::BOOL)\n        {\n            _connect_to_parameter(processor->name(), param->name(), processor->id(), param->id());\n        }\n        else if (type == ParameterType::STRING)\n        {\n            _connect_to_property(processor->name(), param->name(), processor->id(), param->id());\n            // TODO - for now property outputs are always on.\n            _connect_from_property(processor->name(), param->name(), processor->id(), param->id());\n        }\n    }\n}\n\nvoid OSCFrontend::_setup_engine_control()\n{\n    _set_tempo_cp = _osc->add_method(\"/engine/set_tempo\", \"f\", osc::OscMethodType::SET_TEMPO, this->_controller);\n    _set_time_signature_cb = _osc->add_method(\"/engine/set_time_signature\", \"ii\", osc::OscMethodType::SET_TIME_SIGNATURE, this->_controller);\n    _set_playing_mode_cb = _osc->add_method(\"/engine/set_playing_mode\", \"s\", osc::OscMethodType::SET_PLAYING_MODE, this->_controller);\n    _set_sync_mode_cb = _osc->add_method(\"/engine/set_sync_mode\", \"s\", osc::OscMethodType::SET_TEMPO_SYNC_MODE, this->_controller);\n    _set_timing_statistics_enabled_cb = _osc->add_method(\"/engine/set_timing_statistics_enabled\", \"i\",\n                                                         osc::OscMethodType::SET_TIMING_STATISTICS_ENABLED, this->_controller);\n    _reset_timing_statistics_s_cb = _osc->add_method(\"/engine/reset_timing_statistics\", \"s\",\n                                                     osc::OscMethodType::RESET_TIMING_STATISTICS, this->_controller);\n    _reset_timing_statistics_ss_cb = _osc->add_method(\"/engine/reset_timing_statistics\", \"ss\",\n                                                      osc::OscMethodType::RESET_TIMING_STATISTICS, this->_controller);\n}\n\nvoid OSCFrontend::_completion_callback(Event* event, int return_status)\n{\n    ELKLOG_LOG_DEBUG(\"EngineEvent {} completed with status {}({})\",\n                     event->id(),\n                     return_status == 0 ? \"ok\" : \"failure\",\n                     return_status);\n}\n\nvoid OSCFrontend::_start_server()\n{\n    assert(_osc_initialized);\n\n    _running.store(true);\n\n    _osc->run();\n}\n\nvoid OSCFrontend::_stop_server()\n{\n    assert(_osc_initialized);\n\n    _running.store(false);\n    _osc->stop();\n}\n\nbool OSCFrontend::_remove_processor_connections(ObjectId processor_id)\n{\n    assert(_osc_initialized);\n\n    int count = 0;\n    for (const auto& c : _connections)\n    {\n        if (c->processor == processor_id)\n        {\n            _osc->delete_method(c->callback);\n\n            count++;\n        }\n    }\n    _connections.erase(std::remove_if(_connections.begin(),\n                                      _connections.end(),\n                                      [&](const auto& c) { return c->processor == processor_id; }),\n                       _connections.end());\n\n    count += static_cast<int>(_outgoing_connections.erase(static_cast<ObjectId>(processor_id)));\n\n    ELKLOG_LOG_ERROR_IF(count == 0, \"Failed to remove any connections for processor {}\", processor_id)\n    return count > 0;\n}\n\nvoid OSCFrontend::_handle_engine_notification(const EngineNotificationEvent* event)\n{\n    if (event->is_clipping_notification())\n    {\n        _handle_clipping_notification(static_cast<const ClippingNotificationEvent*>(event));\n    }\n    else if (event->is_audio_graph_notification())\n    {\n        _handle_audio_graph_notification(static_cast<const AudioGraphNotificationEvent*>(event));\n    }\n}\n\nvoid OSCFrontend::_handle_param_change_notification(const ParameterChangeNotificationEvent* event)\n{\n    const auto& node = _outgoing_connections.find(event->processor_id());\n    if (node != _outgoing_connections.end())\n    {\n        const auto& param_node = node->second.find(event->parameter_id());\n        if (param_node != node->second.end())\n        {\n            _osc->send(param_node->second.c_str(), event->normalized_value());\n            ELKLOG_LOG_DEBUG(\"Sending parameter change from processor: {}, parameter: {}, value: {}\",\n                             event->processor_id(),\n                             event->parameter_id(),\n                             event->normalized_value());\n        }\n    }\n}\n\nvoid OSCFrontend::_handle_property_change_notification(const PropertyChangeNotificationEvent* event)\n{\n    const auto& node = _outgoing_connections.find(event->processor_id());\n    if (node != _outgoing_connections.end())\n    {\n        const auto& param_node = node->second.find(event->property_id());\n        if (param_node != node->second.end())\n        {\n            _osc->send(param_node->second.c_str(), event->value());\n            ELKLOG_LOG_DEBUG(\"Sending property change from processor: {}, property: {}, value: {}\",\n                             event->processor_id(),\n                             event->property_id(),\n                             event->value());\n        }\n    }\n}\n\nvoid OSCFrontend::_handle_clipping_notification(const ClippingNotificationEvent* event)\n{\n    if (event->channel_type() == ClippingNotificationEvent::ClipChannelType::INPUT)\n    {\n        _osc->send(\"/engine/input_clip_notification\", event->channel());\n    }\n    else if (event->channel_type() == ClippingNotificationEvent::ClipChannelType::OUTPUT)\n    {\n        _osc->send(\"/engine/output_clip_notification\", event->channel());\n    }\n}\n\nvoid OSCFrontend::_handle_audio_graph_notification(const AudioGraphNotificationEvent* event)\n{\n    switch(event->action())\n    {\n        case AudioGraphNotificationEvent::Action::PROCESSOR_CREATED:\n        {\n            ELKLOG_LOG_DEBUG(\"Received a PROCESSOR_CREATED notification for processor {}\", event->processor());\n            auto processor = _processor_container->processor(event->processor());\n            if (processor)\n            {\n                _connect_to_bypass_state(processor.get());\n                _connect_to_program_change(processor.get());\n                _connect_to_parameters_and_properties(processor.get());\n                if(_connect_from_all_parameters && _skip_outputs.count(processor->id()) == false)\n                {\n                    connect_from_processor_parameters(processor->name());\n                    ELKLOG_LOG_INFO(\"Connected OSC callbacks to processor {}\", processor->id());\n                }\n                _skip_outputs.erase(processor->id());\n            }\n            ELKLOG_LOG_ERROR_IF(!processor , \"Processor {} not found\", event->processor())\n            break;\n        }\n\n        case AudioGraphNotificationEvent::Action::TRACK_CREATED:\n        {\n            ELKLOG_LOG_DEBUG(\"Received a TRACK_ADDED notification for track {}\", event->track());\n            auto track = _processor_container->track(event->track());\n            if (track)\n            {\n                _connect_kb_to_track(track.get());\n                _connect_to_bypass_state(track.get());\n                _connect_to_parameters_and_properties(track.get());\n                if(_connect_from_all_parameters && _skip_outputs.count(track->id()) == false)\n                {\n                    connect_from_processor_parameters(track->name());\n                    ELKLOG_LOG_INFO(\"Connected OSC callbacks to track {}\", track->name());\n                }\n                _skip_outputs.erase(track->id());\n            }\n            ELKLOG_LOG_ERROR_IF(!track, \"Track {} not found\", event->track())\n            break;\n        }\n\n        case AudioGraphNotificationEvent::Action::PROCESSOR_DELETED:\n            ELKLOG_LOG_DEBUG(\"Received a PROCESSOR_DELETED notification for processor {}\", event->processor());\n            _remove_processor_connections(event->processor());\n            break;\n\n        case AudioGraphNotificationEvent::Action::TRACK_DELETED:\n            ELKLOG_LOG_DEBUG(\"Received a TRACK_DELETED notification for processor {}\", event->track());\n            _remove_processor_connections(event->track());\n            break;\n\n        default:\n            break;\n    }\n}\n\n} // end namespace control_frontend\n\n} // end namespace sushi::internal\n"
  },
  {
    "path": "src/control_frontends/osc_frontend.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief OSC runtime control frontend\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n *\n * Starts a thread listening for OSC commands at the given port\n * (configurable with proper command sent with apply_command.\n *\n */\n\n#ifndef SUSHI_OSC_FRONTEND_H_H\n#define SUSHI_OSC_FRONTEND_H_H\n\n#include <vector>\n#include <map>\n\n#include \"sushi/control_interface.h\"\n\n#include \"base_control_frontend.h\"\n\nnamespace sushi::internal {\n\nnamespace osc\n{\nclass BaseOscMessenger;\n}\n\nnamespace control_frontend {\n\nclass OSCFrontend;\n\nclass OscState\n{\npublic:\n\n    bool auto_enable_outputs() const;\n\n    void set_auto_enable_outputs(bool value);\n\n    const std::vector<std::pair<std::string, std::vector<ObjectId>>>& enabled_outputs() const;\n\n    void add_enabled_outputs(const std::string& processor_name,\n                             const std::vector<ObjectId>& enabled_parameters);\n\n    void add_enabled_outputs(std::string&& processor_name,\n                             std::vector<ObjectId>&& enabled_parameters);\n\nprivate:\n    bool _auto_enable_outputs;\n    std::vector<std::pair<std::string, std::vector<ObjectId>>> _enabled_outputs;\n};\n\nstruct OscConnection\n{\n    ObjectId           processor;\n    ObjectId           parameter;\n    OSCFrontend*       instance;\n    control::SushiControl* controller;\n\n    void* callback;\n};\n\nclass OSCFrontendAccessor;\n\nclass OSCFrontend : public BaseControlFrontend\n{\npublic:\n    OSCFrontend(engine::BaseEngine* engine,\n                control::SushiControl* controller,\n                osc::BaseOscMessenger* osc_interface);\n\n    ~OSCFrontend() override;\n\n    ControlFrontendStatus init() override;\n\n    /**\n     * @brief Output changes from the given parameter of the given\n     *        processor to osc messages. The output will be on the form:\n     *        \"/parameter/processor_name/parameter_name,f(value)\"\n     * @param processor_name Name of the processor\n     * @param parameter_name Name of the processor's parameter\n     * @return Bool of whether connection succeeded.\n     */\n    bool connect_from_parameter(const std::string& processor_name,\n                                const std::string& parameter_name);\n\n    /**\n     * @brief Stops the broadcasting of OSC messages reflecting changes of a parameter.\n     * @param processor_name Name of the processor\n     * @param parameter_name Name of the processor's parameter\n     * @return Bool of whether disconnection succeeded.\n     */\n    bool disconnect_from_parameter(const std::string& processor_name,\n                                   const std::string& parameter_name);\n\n\n    /**\n     * @brief Enable OSC broadcasting of all parameters from a given processor.\n     * @param processor_name The name of the processor to connect.\n     * @param processor_id The id of the processor to connect.\n     * @return Bool of whether connection succeeded.\n     */\n    bool connect_from_processor_parameters(const std::string& processor_name);\n\n    /**\n     * @brief Register OSC callbacks for all parameters of all plugins.\n     */\n    void connect_from_all_parameters();\n\n    /**\n     * @brief Deregister OSC callbacks for all parameters of all plugins.\n     */\n    void disconnect_from_all_parameters();\n\n    /**\n     * @return Returns all OSC Address Patterns that are currently enabled to output state changes.\n     */\n    std::vector<std::string> get_enabled_parameter_outputs();\n\n    void run() override {_start_server();}\n\n    void stop() override {_stop_server();}\n\n    /* Inherited from EventPoster */\n    int process(Event* event) override;\n\n    std::string send_ip() const;\n\n    int send_port() const;\n\n    int receive_port() const;\n\n    [[nodiscard]] bool get_connect_from_all_parameters() const {return _connect_from_all_parameters;}\n\n    void set_connect_from_all_parameters(bool connect) {_connect_from_all_parameters = connect;}\n\n    OscState save_state() const;\n\n    void set_state(const OscState& state);\n\nprivate:\n    friend OSCFrontendAccessor;\n\n    /**\n     * @brief Connect to control all parameters from a given processor.\n     * @param processor The name of the processor to connect.\n     * @param processor_id The id of the processor to connect.\n     * @return Bool of whether connection succeeded.\n     */\n    void _connect_to_parameters_and_properties(const Processor* processor);\n\n    /**\n     * @brief Connect keyboard messages to a given track.\n     *        The target osc path will be:\n     *        \"/keyboard_event/track_name,sif(note_on/note_off, note_value, velocity)\"\n     * @param processor The track to send to\n     * @return An OscConnection pointer, if one has been created - otherwise nullptr.\n     */\n    OscConnection* _connect_kb_to_track(const Processor* processor);\n\n    /**\n     * @brief Connect osc to the bypass state of a given processor.\n     *        The resulting osc path will be:\n     *        \"/bypass/processor_name,i(enabled == 1, disabled == 0)\"\n     *\n     * @param processor_name Name of the processor\n     * @return An OscConnection pointer, if one has been created - otherwise nullptr.\n     */\n    OscConnection* _connect_to_bypass_state(const Processor* processor);\n\n    /**\n     * @brief Connect program change messages to a specific processor.\n     *        The resulting osc path will be;\n     *        \"/program/processor i (program_id)\"\n     * @param processor Name of the processor\n     * @return An OscConnection pointer, if one has been created - otherwise nullptr.\n     */\n    OscConnection* _connect_to_program_change(const Processor* processor);\n\n    OscConnection* _connect_to_parameter(const std::string& processor_name,\n                                         const std::string& parameter_name,\n                                         ObjectId processor_id,\n                                         ObjectId parameter_id);\n\n    OscConnection* _connect_to_property(const std::string& processor_name,\n                                        const std::string& property_name,\n                                        ObjectId processor_id,\n                                        ObjectId property_id);\n\n    void _connect_from_parameter(const std::string& processor_name,\n                                 const std::string& parameter_name,\n                                 ObjectId processor_id,\n                                 ObjectId parameter_id);\n\n    void _connect_from_property(const std::string& processor_name,\n                                const std::string& property_name,\n                                ObjectId processor_id,\n                                ObjectId property_id);\n\n    void _completion_callback(Event* event, int return_status) override;\n\n    void _start_server();\n\n    void _stop_server();\n\n    void _setup_engine_control();\n\n    bool _remove_processor_connections(ObjectId processor_id);\n\n    std::pair<OscConnection*, std::string> _create_processor_connection(const std::string& processor_name,\n                                                                        ObjectId processor_id,\n                                                                        const std::string& osc_path_prefix);\n\n    void _handle_param_change_notification(const ParameterChangeNotificationEvent* event);\n\n    void _handle_property_change_notification(const PropertyChangeNotificationEvent* event);\n\n    void _handle_engine_notification(const EngineNotificationEvent* event);\n\n    void _handle_audio_graph_notification(const AudioGraphNotificationEvent* event);\n\n    void _handle_clipping_notification(const ClippingNotificationEvent* event);\n\n    bool _connect_from_all_parameters {false};\n\n    std::atomic_bool _osc_initialized {false};\n\n    std::atomic_bool _running {false};\n\n    sushi::control::SushiControl* _controller {nullptr};\n    sushi::control::AudioGraphController* _graph_controller {nullptr};\n    sushi::control::ParameterController*  _param_controller {nullptr};\n\n    const engine::BaseProcessorContainer* _processor_container;\n\n    std::unique_ptr<osc::BaseOscMessenger> _osc {nullptr};\n\n    std::vector<std::unique_ptr<OscConnection>> _connections;\n\n    std::map<ObjectId, std::map<ObjectId, std::string>> _outgoing_connections;\n\n    std::map<ObjectId, bool> _skip_outputs;\n\n    void* _set_tempo_cp {nullptr};\n    void* _set_time_signature_cb {nullptr};\n    void* _set_playing_mode_cb {nullptr};\n    void* _set_sync_mode_cb {nullptr};\n    void* _set_timing_statistics_enabled_cb {nullptr};\n    void* _reset_timing_statistics_s_cb {nullptr};\n    void* _reset_timing_statistics_ss_cb {nullptr};\n};\n\n} // end namespace control_frontend\n} // end namespace sushi::internal\n\n#endif // SUSHI_OSC_FRONTEND_H_H\n"
  },
  {
    "path": "src/control_frontends/osc_utils.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief OSC utilities\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_OSC_UTILS_H\n#define SUSHI_OSC_UTILS_H\n\n#include <sstream>\n\n#include \"elklog/static_logger.h\"\n\n#include \"osc_frontend.h\"\n\n\nnamespace sushi::internal::osc {\n\nenum class OscMethodType\n{\n    SEND_PARAMETER_CHANGE_EVENT,\n    SEND_PROPERTY_CHANGE_EVENT,\n    SEND_BYPASS_STATE_EVENT,\n    SEND_KEYBOARD_NOTE_EVENT,\n    SEND_KEYBOARD_MODULATION_EVENT,\n    SEND_PROGRAM_CHANGE_EVENT,\n    SET_TEMPO,\n    SET_TIME_SIGNATURE,\n    SET_PLAYING_MODE,\n    SET_TEMPO_SYNC_MODE,\n    SET_TIMING_STATISTICS_ENABLED,\n    RESET_TIMING_STATISTICS,\n    NONE\n};\n\nclass BaseOscMessenger\n{\npublic:\n    BaseOscMessenger(int receive_port,\n                     int send_port,\n                     const std::string& send_ip) : _receive_port(receive_port),\n                                                   _send_port(send_port),\n                                                   _send_ip(send_ip)\n    {}\n\n    virtual ~BaseOscMessenger() = default;\n\n    /**\n     * Call before using the class instance.\n     * @return Whether initialization succeeded.\n     */\n    virtual bool init() = 0;\n\n    /**\n     * Starts the OSC messaging thread.\n     */\n    virtual void run() = 0;\n\n    /**\n     * Stops the OSC messaging thread.\n     */\n    virtual void stop() = 0;\n\n    /**\n     * Subscribes to callbacks, triggered when the specified OSC\n     * \"Address Pattern\" and \"Type Tag String\" combination are received.\n     * @param address_pattern The Address Pattern to look for\n     * @param type_tag_string  The Type Tag String to look for\n     * @param type The OscMethodType enum - specifying what category the message is.\n     *             Necessary if the OSC implementation uses a different callback for each type.\n     * @param callback_data A void* of \"user_data\", which can vary depending on the callback_data type.\n     *                      The actual payload need only be known in the corresponding callback passed.\n     * @return A unique handle which identifies the added method.\n     */\n    virtual void* add_method(const char* address_pattern,\n                             const char* type_tag_string,\n                             OscMethodType type,\n                             const void* callback_data) = 0;\n\n    /**\n     * Deletes the connection to a specific callback, created with add_method.\n     * This method signature uses a void* handle, so that it can be compatible with many OSC libraries.\n     * But if we will only ever use oscpack, it can be changed to uint_64_t.\n     * @param handle Needs to be the void* returned from add_method.\n     */\n    virtual void delete_method(void* handle) = 0;\n\n    /**\n     * Send a single OSC message. Currently only TTS: \"i\" or \"f\" are supported.\n     * @param address_pattern  The address pattern to send to.\n     * @param payload The values of the message.\n     */\n    virtual void send(const char* address_pattern, int payload) = 0;\n    virtual void send(const char* address_pattern, float payload) = 0;\n    virtual void send(const char* address_pattern, const std::string& payload) = 0;\n\n    std::string send_ip() const\n    {\n        return _send_ip;\n    }\n\n    int send_port() const\n    {\n        return _send_port;\n    }\n\n    int receive_port() const\n    {\n        return _receive_port;\n    }\n\nprotected:\n    int _receive_port;\n    int _send_port;\n    std::string _send_ip;\n\n    std::atomic_bool _osc_initialized {false};\n};\n\n/**\n * @brief Ensure that a string is safe to use as an OSC path by stripping illegal\n *        characters and replacing spaces with underscores.\n * @param name The string to process\n * @return an std::string safe to use as an OSC path\n */\nstd::string make_safe_path(std::string name);\n\n} // end namespace sushi::internal::osc\n\n#endif // SUSHI_OSC_UTILS_H\n"
  },
  {
    "path": "src/control_frontends/oscpack_osc_messenger.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include <sstream>\n\n#include \"elklog/static_logger.h\"\n\n#include \"osc_utils.h\"\n#include \"oscpack_osc_messenger.h\"\n\nnamespace sushi::internal {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"osc frontend\");\n\nnamespace osc\n{\n\n// The below are informed by this:\n// \"According to the C++0x standard, section 20.3.3.26, std::pair has an operator< defined such that for two pairs x and y, it returns:\"\n// x.first < y.first || (!(y.first < x.first) && x.second < y.second)\n\n// These two suffice - it doesn't require ==, or >, =>, etc.\n// If I didn't use LightKey, but std::pair, my overrides would not be hit.\n// Stepping through, I've ensured the below engage the string .compare(char*) overload.\nbool operator<(const std::pair<std::string, std::string>& fat, const LightKey& light)\n{\n    return fat.first < light.first || (!(light.first < fat.first) && fat.second < light.second);\n}\n\nbool operator<(const LightKey& light, const std::pair<std::string, std::string>& fat)\n{\n    return light.first < fat.first || (!(fat.first < light.first) && light.second < fat.second);\n}\n\nOscpackOscMessenger::OscpackOscMessenger(int receive_port,\n                                         int send_port,\n                                         const std::string& send_ip) : BaseOscMessenger(receive_port,\n                                                                                        send_port,\n                                                                                        send_ip)\n{}\n\nOscpackOscMessenger::~OscpackOscMessenger()\n{\n    if (_osc_initialized)\n    {\n        _osc_initialized = false;\n    }\n}\n\nbool OscpackOscMessenger::init()\n{\n    bool status = true;\n\n    try\n    {\n        _transmit_socket = std::make_unique<UdpTransmitSocket>(IpEndpointName(_send_ip.c_str(), _send_port));\n    }\n    catch ([[maybe_unused]] std::exception& e)\n    {\n        status = false;\n        ELKLOG_LOG_ERROR(\"OSC transmitter failed instantiating for IP {} and port {}, with message: \",\n                        _send_ip.c_str(),\n                        _send_port,\n                        e.what());\n    }\n\n    try\n    {\n        _receive_socket = std::make_unique<UdpListeningReceiveSocket>(IpEndpointName(IpEndpointName::ANY_ADDRESS, _receive_port),\n                                                                      this);\n    }\n    catch ([[maybe_unused]] std::exception& e)\n    {\n        status = false;\n        ELKLOG_LOG_ERROR(\"OSC receiver failed instantiating for port {}, with message: \",\n                        _receive_port,\n                        e.what());\n    }\n\n    return status;\n}\n\nvoid OscpackOscMessenger::run()\n{\n    _osc_receive_worker = std::thread(&OscpackOscMessenger::_osc_receiving_worker, this);\n}\n\nvoid OscpackOscMessenger::stop()\n{\n    if (_receive_socket.get() != nullptr)\n    {\n        _receive_socket->AsynchronousBreak();\n    }\n\n    if (_osc_receive_worker.joinable())\n    {\n        _osc_receive_worker.join();\n    }\n}\n\nvoid* OscpackOscMessenger::add_method(const char* address_pattern,\n                                      const char* type_tag_string,\n                                      OscMethodType type,\n                                      const void* callback_data)\n{\n    LightKey key(address_pattern,  type_tag_string);\n    auto iterator = _registered_messages.find(key);\n\n    if (iterator != _registered_messages.end())\n    {\n        return reinterpret_cast<void*>(-1);\n    }\n\n    MessageRegistration reg;\n\n    // Casting away const to address OSC library API incompatibilities.\n    // It is later used unchanged.\n    reg.callback_data = const_cast<void*>(callback_data);\n\n    reg.type = type;\n    reg.handle = _last_generated_handle;\n\n    _registered_messages[{address_pattern, type_tag_string}] = reg;\n\n    _last_generated_handle++;\n\n    auto to_return = reinterpret_cast<void*>(reg.handle);\n    assert(sizeof(void*) == sizeof(OSC_CALLBACK_HANDLE));\n    return to_return;\n}\n\nvoid OscpackOscMessenger::delete_method(void* handle)\n{\n    auto itr = _registered_messages.begin();\n    while (itr != _registered_messages.end())\n    {\n        auto id = itr->second.handle;\n        auto method_int = reinterpret_cast<OSC_CALLBACK_HANDLE>(handle);\n        if (id == method_int)\n        {\n            _registered_messages.erase(itr);\n            break;\n        }\n        else\n        {\n            ++itr;\n        }\n    }\n}\n\nvoid OscpackOscMessenger::send(const char* address_pattern, float payload)\n{\n    oscpack::OutboundPacketStream p(_output_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << payload  << oscpack::EndMessage;\n    _transmit_socket->Send(p.Data(), p.Size());\n}\n\nvoid OscpackOscMessenger::send(const char* address_pattern, int payload)\n{\n    oscpack::OutboundPacketStream p(_output_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << payload  << oscpack::EndMessage;\n    _transmit_socket->Send(p.Data(), p.Size());\n}\n\nvoid OscpackOscMessenger::send(const char* address_pattern, const std::string& payload)\n{\n    oscpack::OutboundPacketStream p(_output_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << payload.c_str()  << oscpack::EndMessage;\n    _transmit_socket->Send(p.Data(), p.Size());\n}\n\nvoid OscpackOscMessenger::ProcessMessage(const oscpack::ReceivedMessage& m, const IpEndpointName& /*remoteEndpoint*/)\n{\n    try\n    {\n        LightKey key(m.AddressPattern(),  m.TypeTags());\n        auto iterator = _registered_messages.find(key);\n\n        if (iterator != _registered_messages.end())\n        {\n            auto reg = iterator->second;\n\n            switch (reg.type)\n            {\n                case OscMethodType::SEND_PARAMETER_CHANGE_EVENT:\n                {\n                    _send_parameter_change_event(m, reg.callback_data);\n                    break;\n                }\n                case OscMethodType::SEND_PROPERTY_CHANGE_EVENT:\n                {\n                    _send_property_change_event(m, reg.callback_data);\n                    break;\n                }\n                case OscMethodType::SEND_BYPASS_STATE_EVENT:\n                {\n                    _send_bypass_state_event(m, reg.callback_data);\n                    break;\n                }\n                case OscMethodType::SEND_KEYBOARD_NOTE_EVENT:\n                {\n                    _send_keyboard_note_event(m, reg.callback_data);\n                    break;\n                }\n                case OscMethodType::SEND_KEYBOARD_MODULATION_EVENT:\n                {\n                    _send_keyboard_modulation_event(m, reg.callback_data);\n                    break;\n                }\n                case OscMethodType::SEND_PROGRAM_CHANGE_EVENT:\n                {\n                    _send_program_change_event(m, reg.callback_data);\n                    break;\n                }\n                case OscMethodType::SET_TEMPO:\n                {\n                    _set_tempo(m, reg.callback_data);\n                    break;\n                }\n                case OscMethodType::SET_TIME_SIGNATURE:\n                {\n                    _set_time_signature(m, reg.callback_data);\n                    break;\n                }\n                case OscMethodType::SET_PLAYING_MODE:\n                {\n                    _set_playing_mode(m, reg.callback_data);\n                    break;\n                }\n                case OscMethodType::SET_TEMPO_SYNC_MODE:\n                {\n                    _set_tempo_sync_mode(m, reg.callback_data);\n                    break;\n                }\n                case OscMethodType::SET_TIMING_STATISTICS_ENABLED:\n                {\n                    _set_timing_statistics_enabled(m, reg.callback_data);\n                    break;\n                }\n                case OscMethodType::RESET_TIMING_STATISTICS:\n                {\n                    _reset_timing_statistics(m, reg.callback_data);\n                    break;\n                }\n                default:\n                {\n                    ELKLOG_LOG_INFO(\"Unrecognised OSC message received: {}\", m.AddressPattern());\n                }\n            }\n        }\n    }\n    catch ([[maybe_unused]] oscpack::Exception& e)\n    {\n        // Any parsing errors such as unexpected argument types, or missing arguments get thrown as exceptions.\n        ELKLOG_LOG_ERROR(\"Exception while parsing message: {}: {}\", m.AddressPattern(), e.what());\n    }\n}\n\nvoid OscpackOscMessenger::_osc_receiving_worker()\n{\n    _receive_socket->Run();\n}\n\nvoid OscpackOscMessenger::_send_parameter_change_event(const oscpack::ReceivedMessage& m, void* user_data) const\n{\n    oscpack::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();\n    float value = (arg++)->AsFloat();\n    auto connection = static_cast<control_frontend::OscConnection*>(user_data);\n    auto controller = connection->controller->parameter_controller();\n    controller->set_parameter_value(connection->processor, connection->parameter, value);\n\n    ELKLOG_LOG_DEBUG(\"Sending parameter {} on processor {} change to {}.\", connection->parameter, connection->processor, value);\n}\n\nvoid OscpackOscMessenger::_send_property_change_event(const oscpack::ReceivedMessage& m, void* user_data) const\n{\n    oscpack::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();\n    auto value = (arg++)->AsString();\n    auto connection = static_cast<control_frontend::OscConnection*>(user_data);\n    auto controller = connection->controller->parameter_controller();\n    controller->set_property_value(connection->processor, connection->parameter, value);\n\n    ELKLOG_LOG_DEBUG(\"Sending property {} on processor {} change to {}.\", connection->parameter, connection->processor, value);\n}\n\nvoid OscpackOscMessenger::_send_bypass_state_event(const oscpack::ReceivedMessage& m, void* user_data) const\n{\n    oscpack::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();\n    int value = (arg++)->AsInt32();\n    bool isBypassed = (value) ? true : false;\n\n    auto connection = static_cast<control_frontend::OscConnection*>(user_data);\n    auto controller = connection->controller->audio_graph_controller();\n    controller->set_processor_bypass_state(connection->processor, isBypassed);\n\n    ELKLOG_LOG_DEBUG(\"Setting processor {} bypass to {}\", connection->processor, isBypassed);\n}\n\nvoid OscpackOscMessenger::_send_keyboard_note_event(const oscpack::ReceivedMessage& m, void* user_data) const\n{\n    oscpack::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();\n    std::string_view event = (arg++)->AsString();\n    int channel = (arg++)->AsInt32();\n    int note = (arg++)->AsInt32();\n    float value = (arg++)->AsFloat();\n\n    auto connection = static_cast<control_frontend::OscConnection*>(user_data);\n    auto controller = connection->controller->keyboard_controller();\n\n    if (event == \"note_on\")\n    {\n        controller->send_note_on(connection->processor, channel, note, value);\n    }\n    else if (event == \"note_off\")\n    {\n        controller->send_note_off(connection->processor, channel, note, value);\n    }\n    else if (event == \"note_aftertouch\")\n    {\n        controller->send_note_aftertouch(connection->processor, channel, note, value);\n    }\n    else\n    {\n        ELKLOG_LOG_WARNING(\"Unrecognized event: {}.\", event);\n    }\n    ELKLOG_LOG_DEBUG(\"Sending {} on processor {}.\", event, connection->processor);\n}\n\nvoid OscpackOscMessenger::_send_keyboard_modulation_event(const oscpack::ReceivedMessage& m, void* user_data) const\n{\n    oscpack::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();\n    std::string_view event = (arg++)->AsString();\n    int channel = (arg++)->AsInt32();\n    float value = (arg++)->AsFloat();\n\n    auto connection = static_cast<control_frontend::OscConnection*>(user_data);\n    auto controller = connection->controller->keyboard_controller();\n\n    if (event == \"modulation\")\n    {\n        controller->send_modulation(connection->processor, channel, value);\n    }\n    else if (event == \"pitch_bend\")\n    {\n        controller->send_pitch_bend(connection->processor, channel, value);\n    }\n    else if (event == \"aftertouch\")\n    {\n        controller->send_aftertouch(connection->processor, channel, value);\n    }\n    else\n    {\n        ELKLOG_LOG_WARNING(\"Unrecognized event: {}.\", event);\n    }\n    ELKLOG_LOG_DEBUG(\"Sending {} on processor {}.\", event, connection->processor);\n}\n\nvoid OscpackOscMessenger::_send_program_change_event(const oscpack::ReceivedMessage& m, void* user_data) const\n{\n    oscpack::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();\n    int program_id = (arg++)->AsInt32();\n\n    auto connection = static_cast<control_frontend::OscConnection*>(user_data);\n    auto controller = connection->controller->program_controller();\n    controller->set_processor_program(connection->processor, program_id);\n\n    ELKLOG_LOG_DEBUG(\"Sending change to program {}, on processor {}\", program_id, connection->processor);\n}\n\nvoid OscpackOscMessenger::_set_timing_statistics_enabled(const oscpack::ReceivedMessage& m, void* user_data) const\n{\n    oscpack::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();\n    int value = (arg++)->AsInt32();\n    bool is_enabled = (value) ? true : false;\n\n    auto controller = static_cast<control::SushiControl*>(user_data)->timing_controller();\n    controller->set_timing_statistics_enabled(is_enabled);\n\n    ELKLOG_LOG_DEBUG(\"Got request to set timing statistics enabled to {}\", is_enabled);\n}\n\nvoid OscpackOscMessenger::_reset_timing_statistics(const oscpack::ReceivedMessage& m, void* user_data) const\n{\n    oscpack::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();\n    std::string output_text = (arg++)->AsString();\n    std::string_view type = output_text;\n\n    auto controller = static_cast<control::SushiControl*>(user_data);\n    auto timing_ctrl = controller->timing_controller();\n    auto processor_ctrl = controller->audio_graph_controller();\n\n    if (type == \"all\")\n    {\n        auto status = timing_ctrl->reset_all_timings();\n        if (status != control::ControlStatus::OK)\n        {\n            ELKLOG_LOG_WARNING(\"Failed to reset track timings of all tracks and processors\");\n        }\n    }\n    else if (type == \"track\")\n    {\n        std::string track_name = (arg++)->AsString();\n\n        auto [track_status, track_id] = processor_ctrl->get_track_id(track_name);\n        if (track_status == control::ControlStatus::OK)\n        {\n            output_text += \" \" + track_name;\n            timing_ctrl->reset_track_timings(track_id);\n        }\n        else\n        {\n            ELKLOG_LOG_WARNING(\"No track with name {} available\", track_name);\n        }\n    }\n    else if (type == \"processor\")\n    {\n        std::string processor_name = (arg++)->AsString();\n\n        auto [processor_status, processor_id] = processor_ctrl->get_processor_id(processor_name);\n        if (processor_status == control::ControlStatus::OK)\n        {\n            output_text += \" \" + processor_name;\n            timing_ctrl->reset_processor_timings(processor_id);\n        }\n        else\n        {\n            ELKLOG_LOG_WARNING(\"No processor with name {} available\", processor_name);\n        }\n    }\n    else\n    {\n        ELKLOG_LOG_WARNING(\"Unrecognized reset target\");\n    }\n    ELKLOG_LOG_DEBUG(\"Resetting {} timing statistics\", output_text);\n}\n\nvoid OscpackOscMessenger::_set_tempo(const oscpack::ReceivedMessage& m, void* user_data) const\n{\n    oscpack::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();\n    float tempo = (arg++)->AsFloat();\n\n    auto controller = static_cast<control::SushiControl*>(user_data)->transport_controller();\n    controller->set_tempo(tempo);\n\n    ELKLOG_LOG_DEBUG(\"Got a set tempo request to {} bpm\", tempo);\n}\n\nvoid OscpackOscMessenger::_set_time_signature(const oscpack::ReceivedMessage& m, void* user_data) const\n{\n    oscpack::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();\n    int numerator = (arg++)->AsInt32();\n    int denominator = (arg++)->AsInt32();\n\n    auto controller = static_cast<control::SushiControl*>(user_data)->transport_controller();\n    controller->set_time_signature({numerator, denominator});\n\n    ELKLOG_LOG_DEBUG(\"Got a set time signature to {}/{} request\", numerator, denominator);\n}\n\nvoid OscpackOscMessenger::_set_playing_mode(const oscpack::ReceivedMessage& m, void* user_data) const\n{\n    oscpack::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();\n    std::string mode_str = (arg++)->AsString();\n\n    auto controller = static_cast<control::SushiControl*>(user_data)->transport_controller();\n\n    if (mode_str == \"playing\")\n    {\n        controller->set_playing_mode(control::PlayingMode::PLAYING);\n    }\n    else if (mode_str == \"stopped\")\n    {\n        controller->set_playing_mode(control::PlayingMode::STOPPED);\n    }\n    else\n    {\n        ELKLOG_LOG_INFO(\"Unrecognised playing mode \\\"{}\\\" received\", mode_str);\n    }\n\n    ELKLOG_LOG_DEBUG(\"Got a set playing mode {} request\", mode_str);\n}\n\nvoid OscpackOscMessenger::_set_tempo_sync_mode(const oscpack::ReceivedMessage& m, void* user_data) const\n{\n    oscpack::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();\n    std::string mode_str = (arg++)->AsString();\n\n    auto controller = static_cast<control::SushiControl*>(user_data)->transport_controller();\n\n    if (mode_str == \"internal\")\n    {\n        controller->set_sync_mode(control::SyncMode::INTERNAL);\n    }\n    else if (mode_str == \"ableton_link\")\n    {\n        controller->set_sync_mode(control::SyncMode::LINK);\n    }\n    else if (mode_str == \"midi\")\n    {\n        controller->set_sync_mode(control::SyncMode::MIDI);\n    }\n    else\n    {\n        ELKLOG_LOG_INFO(\"Unrecognised sync mode \\\"{}\\\" received\", mode_str);\n    }\n\n    ELKLOG_LOG_DEBUG(\"Got a set sync mode to {} request\", mode_str);\n}\n\n} // end namespace osc\n} // end namespace sushi::internal\n"
  },
  {
    "path": "src/control_frontends/oscpack_osc_messenger.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief OSCPACK OSC library wrapper\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_OSCPACK_OSC_MESSENGER_H\n#define SUSHI_OSCPACK_OSC_MESSENGER_H\n\n#include <sstream>\n#include <iostream>\n\n#include \"elklog/static_logger.h\"\n\n#include \"third-party/oscpack/osc/OscReceivedElements.h\"\n#include \"third-party/oscpack/osc/OscOutboundPacketStream.h\"\n\n#ifndef OSCPACK_UNIT_TESTS\n#include \"third-party/oscpack/osc/OscPacketListener.h\"\n#include \"third-party/oscpack/ip/UdpSocket.h\"\n#endif\n\n#include \"osc_frontend.h\"\n#include \"osc_utils.h\"\n\nnamespace oscpack = ::osc;\n\nnamespace sushi::internal::osc\n{\n\nstruct LightKey\n{\n    LightKey(const char* first_, const char* second_): first(first_), second(second_) {}\n    const char* first;\n    const char* second;\n};\n\nbool operator<(const std::pair<std::string, std::string>& fat, const LightKey& light);\nbool operator<(const LightKey& light, const std::pair<std::string, std::string>& fat);\n\n// In OscPack they have an incoming packet size max of 4098.\n// But that's already allocated when we receive it - they seem to not enforce any size for\n// sending.\n// 1512 is the common default MTU - UDP headers are 8 bytes fixed size, giving he below.\nconstexpr size_t OSC_OUTPUT_BUFFER_SIZE = 1504;\n\n// We need to be able to cast between OSC_CALLBACK_HANDLE, and void*, to keep the API in BaseOscMessenger consistent.\n// If we are OK with breaking the API compatibility with Liblo, we can just change the API to directly expose uint64_t.\n#include <cstdint>\n#if INTPTR_MAX == INT32_MAX\nusing OSC_CALLBACK_HANDLE = uint32_t;\n#elif INTPTR_MAX == INT64_MAX\nusing OSC_CALLBACK_HANDLE = uint64_t;\n#else\n#error \"Environment not 32 or 64-bit.\"\n#endif\n\nclass Accessor;\n\nclass OscpackOscMessenger : public BaseOscMessenger,\n                            public oscpack::OscPacketListener\n{\npublic:\n    OscpackOscMessenger(int receive_port, int send_port, const std::string& send_ip);\n\n    ~OscpackOscMessenger() override;\n\n    bool init() override;\n\n    void run() override;\n    void stop() override;\n\n    void* add_method(const char* address_pattern,\n                     const char* type_tag_string,\n                     OscMethodType type,\n                     const void* callback_data) override;\n\n    void delete_method(void* handle) override;\n\n    void send(const char* address_pattern, float payload) override;\n\n    void send(const char* address_pattern, int payload) override;\n\n    void send(const char* address_pattern, const std::string& payload) override;\n\nprotected:\n    /**\n     * Defined in osc::OscPacketListener.\n     */\n    void ProcessMessage(const oscpack::ReceivedMessage& m, const IpEndpointName& /*remoteEndpoint*/) override;\n\nprivate:\n    friend Accessor;\n\n    void _osc_receiving_worker();\n\n    void _send_parameter_change_event(const oscpack::ReceivedMessage& m, void* user_data) const;\n    void _send_property_change_event(const oscpack::ReceivedMessage& m, void* user_data) const;\n    void _send_bypass_state_event(const oscpack::ReceivedMessage& m, void* user_data) const;\n    void _send_keyboard_note_event(const oscpack::ReceivedMessage& m, void* user_data) const;\n    void _send_keyboard_modulation_event(const oscpack::ReceivedMessage& m, void* user_data) const;\n    void _send_program_change_event(const oscpack::ReceivedMessage& m, void* user_data) const;\n    void _set_timing_statistics_enabled(const oscpack::ReceivedMessage& m, void* user_data) const;\n    void _reset_timing_statistics(const oscpack::ReceivedMessage& m, void* user_data) const;\n    void _set_tempo(const oscpack::ReceivedMessage& m, void* user_data) const;\n    void _set_time_signature(const oscpack::ReceivedMessage& m, void* user_data) const;\n    void _set_playing_mode(const oscpack::ReceivedMessage& m, void* user_data) const;\n    void _set_tempo_sync_mode(const oscpack::ReceivedMessage& m, void* user_data) const;\n\n    std::thread _osc_receive_worker;\n    std::unique_ptr<UdpTransmitSocket> _transmit_socket {nullptr};\n\n    std::unique_ptr<UdpListeningReceiveSocket> _receive_socket {nullptr};\n\n    struct MessageRegistration\n    {\n        void* callback_data {nullptr};\n        OscMethodType type {OscMethodType::NONE};\n        OSC_CALLBACK_HANDLE handle {0};\n    };\n\n    using RegisteredMessages = std::map<std::pair<std::string, std::string>, MessageRegistration, std::less<>> ;\n\n    RegisteredMessages _registered_messages;\n\n    OSC_CALLBACK_HANDLE _last_generated_handle {0};\n\n    char _output_buffer[OSC_OUTPUT_BUFFER_SIZE];\n};\n\n} // end namespace sushi::internal::osc\n\n#endif // SUSHI_OSCPACK_OSC_MESSENGER_H\n"
  },
  {
    "path": "src/control_frontends/reactive_midi_frontend.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Reactive MIDI frontend\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <chrono>\n\n#include \"elklog/static_logger.h\"\n\n#include \"reactive_midi_frontend.h\"\n\n#include \"library/midi_decoder.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"reactive midi frontend\");\n\nnamespace sushi::internal::midi_frontend {\n\nReactiveMidiFrontend::ReactiveMidiFrontend(midi_receiver::MidiReceiver* dispatcher)\n        : BaseMidiFrontend(dispatcher)\n{}\n\nReactiveMidiFrontend::~ReactiveMidiFrontend()\n{\n    stop();\n}\n\nbool ReactiveMidiFrontend::init()\n{\n    return true;\n}\n\nvoid ReactiveMidiFrontend::run()\n{\n\n}\n\nvoid ReactiveMidiFrontend::stop()\n{\n\n}\n\nvoid ReactiveMidiFrontend::receive_midi(int input, MidiDataByte data, Time timestamp)\n{\n    _receiver->send_midi(input, data, timestamp);\n\n    ELKLOG_LOG_DEBUG(\"Received midi message: [{:x} {:x} {:x} {:x}], port{}, timestamp: {}\",\n                    data[0], data[1], data[2], data[3], input, timestamp.count());\n}\n\nvoid ReactiveMidiFrontend::send_midi(int output, MidiDataByte data, Time timestamp)\n{\n    if (_callback)\n    {\n        _callback(output, data, timestamp);\n    }\n    else\n    {\n        ELKLOG_LOG_DEBUG(\"ReactiveMidiFrontend::send_midi was invoked on a frontend instance,\"\n                        \" which has no sending _callback. \"\n                        \"First pass one to the frontend using set_callback(...).\");\n    }\n}\n\nvoid ReactiveMidiFrontend::set_callback(ReactiveMidiCallback&& callback)\n{\n    _callback = std::move(callback);\n}\n\n} // end namespace sushi::internal::midi_frontend\n\n"
  },
  {
    "path": "src/control_frontends/reactive_midi_frontend.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n  * @brief Reactive MIDI frontend, provides a frontend for getting midi messages into the engine\n  * @copyright 2017-2023 Elk Audio AB, Stockholm\n  */\n\n#ifndef SUSHI_REACTIVE_MIDI_FRONTEND_H\n#define SUSHI_REACTIVE_MIDI_FRONTEND_H\n\n#include <functional>\n\n#include \"sushi/sushi_time.h\"\n\n#include \"base_midi_frontend.h\"\n\nnamespace sushi::internal::midi_frontend {\n\n/**\n * @brief Callback signature for method to invoke to notify host of any new MIDI message received.\n */\nusing ReactiveMidiCallback = std::function<void(int output, MidiDataByte data, Time timestamp)>;\n\n/**\n * @brief A frontend for MIDI messaging which is to be used when Sushi is included in a hosting audio app/plugin,\n * as a library.\n *\n * The current implementation assumes this will only involve input over a single MIDI device, meaning support for\n * multiple inputs and/or outputs is omitted.\n *\n */\nclass ReactiveMidiFrontend : public BaseMidiFrontend\n{\npublic:\n    ReactiveMidiFrontend(midi_receiver::MidiReceiver* dispatcher);\n\n    ~ReactiveMidiFrontend() override;\n\n    bool init() override;\n\n    void run() override;\n\n    void stop() override;\n\n    void send_midi(int output,\n                   MidiDataByte data,\n                   [[maybe_unused]]Time timestamp) override;\n\n    /**\n     * The embedding host uses this method to pass any incoming MIDI messages to Sushi.\n     * @param input Currently assumed to always be 0 since the frontend only supports a single input device.\n     * @param data MidiDataByte\n     * @param timestamp Sushi Time timestamp for message\n     */\n    void receive_midi(int input, MidiDataByte data, Time timestamp);\n\n    /**\n     * For passing a callback of type ReactiveMidiCallback to the frontend.\n     * @param callback\n     */\n    void set_callback(ReactiveMidiCallback&& callback);\n\nprivate:\n    ReactiveMidiCallback _callback;\n};\n\n} // end namespace sushi::internal::midi_frontend\n\n#endif // SUSHI_REACTIVE_MIDI_FRONTEND_H_H\n"
  },
  {
    "path": "src/control_frontends/rt_midi_frontend.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief RT midi frontend\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <functional>\n#include <tuple>\n\n#include \"elklog/static_logger.h\"\n\n#include \"rt_midi_frontend.h\"\n#include \"sushi/sushi_time.h\"\n#include \"library/midi_decoder.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"rtmidi\");\n\nusing RtMidiFunction = std::function<void(double deltatime, std::vector<unsigned char> message, void* user_data)>;\n\nconstexpr int RTMIDI_MESSAGE_SIZE = 3;\n\nnamespace sushi::internal::midi_frontend {\n\nvoid midi_callback([[maybe_unused]]double deltatime, std::vector<unsigned char>* message, void* user_data)\n{\n    auto* callback_data = static_cast<RtMidiCallbackData*>(user_data);\n    int byte_count = static_cast<int>(message->size());\n    if (byte_count > 0)\n    {\n        const uint8_t* data_buffer = static_cast<const uint8_t*>(message->data());\n        Time timestamp = IMMEDIATE_PROCESS;\n        callback_data->receiver->send_midi(callback_data->input_number, midi::to_midi_data_byte(data_buffer, byte_count), timestamp);\n\n        ELKLOG_LOG_DEBUG(\"Received midi message: [{:x} {:x} {:x} {:x}], port{}, timestamp: {}\",\n                         data_buffer[0], data_buffer[1], data_buffer[2], data_buffer[3], callback_data->input_number, timestamp.count());\n    }\n    ELKLOG_LOG_WARNING_IF(byte_count < 0, \"Decoder returned {}\", strerror(-byte_count))\n}\n\nRtMidiFrontend::RtMidiFrontend(int inputs,\n                               int outputs,\n                               std::vector<std::tuple<int, int, bool>> input_mappings,\n                               std::vector<std::tuple<int, int, bool>> output_mappings,\n                               midi_receiver::MidiReceiver* dispatcher) : BaseMidiFrontend(dispatcher),\n                                                                          _inputs(inputs),\n                                                                          _outputs(outputs),\n                                                                          _input_mappings(input_mappings),\n                                                                          _output_mappings(output_mappings)\n{\n\n}\n\nRtMidiFrontend::~RtMidiFrontend()\n{\n    stop();\n}\n\nbool RtMidiFrontend::init()\n{\n    // Set up inputs\n    for (int i = 0; i < _inputs; i++)\n    {\n        try\n        {\n            auto& callback_data = _input_midi_ports.emplace_back();\n            callback_data.input_number = i;\n            callback_data.receiver = _receiver;\n        }\n        catch (RtMidiError& error)\n        {\n            ELKLOG_LOG_WARNING(\"Failed to create midi input port for input {}: {}\", i, error.getMessage());\n            return false;\n        }\n    }\n\n    // Set up outputs\n    for (int i = 0; i < _outputs; i++)\n    {\n        try\n        {\n            _output_midi_ports.emplace_back();\n        }\n        catch (RtMidiError& error)\n        {\n            ELKLOG_LOG_WARNING(\"Failed to create midi output port for output {}: {}\", i, error.getMessage());\n            return false;\n        }\n    }\n\n    // If no I/O mappings were given, create a sensible default if MIDI ports are available\n    if ( (_inputs > 0) && (_input_midi_ports[0].midi_input.getPortCount() > 0) && (_input_mappings.size() == 0) )\n    {\n        for (int i = 0; i < _inputs; i++)\n        {\n            _input_mappings.emplace_back(std::make_tuple(i, i, false));\n            ELKLOG_LOG_INFO(\"Adding default mapping for MIDI input device {}\", i);\n        }\n    }\n    if ( (_outputs > 0) && (_output_midi_ports[0].getPortCount() > 0) &&  (_output_mappings.size() == 0) )\n    {\n        for (int i = 0; i < _outputs; i++)\n        {\n            _output_mappings.emplace_back(std::make_tuple(i, i, false));\n            ELKLOG_LOG_INFO(\"Adding default mapping for MIDI output device {}\", i);\n        }\n    }\n\n    // Create input ports\n    for (auto& [rt_midi_device, sushi_midi_port, virtual_port] : _input_mappings)\n    {\n        try\n        {\n            auto& input = _input_midi_ports[sushi_midi_port];\n            if (virtual_port)\n            {\n                input.midi_input.openVirtualPort(\"Sushi virtual port \" + std::to_string(rt_midi_device));\n                input.midi_input.setCallback(midi_callback, static_cast<void*>(&input));\n                ELKLOG_LOG_INFO(\"Midi input {} connected to sushi virtual port {}\", sushi_midi_port, rt_midi_device);\n            }\n            else\n            {\n                input.midi_input.openPort(rt_midi_device);\n                input.midi_input.setCallback(midi_callback, static_cast<void*>(&input));\n                ELKLOG_LOG_INFO(\"Midi input {} connected to {}\", sushi_midi_port, input.midi_input.getPortName(rt_midi_device));\n            }\n        }\n        catch(RtMidiError& error)\n        {\n            ELKLOG_LOG_WARNING(\"Failed to connect midi input {} to RtMidi device with index {}: {}\", sushi_midi_port, rt_midi_device, error.getMessage());\n            return false;\n        }\n    }\n\n    // Create output ports\n    for (auto& output_mapping : _output_mappings)\n    {\n        int rt_midi_device = std::get<0>(output_mapping);\n        int sushi_midi_port = std::get<1>(output_mapping);\n        bool virtual_port = std::get<2>(output_mapping);\n        try\n        {\n            auto& output = _output_midi_ports[sushi_midi_port];\n            if (virtual_port)\n            {\n                output.openVirtualPort(\"Sushi virtual port \" + std::to_string(rt_midi_device));\n                ELKLOG_LOG_INFO(\"Midi output {} connected to sushi virtual port {}\", sushi_midi_port, rt_midi_device);\n            }\n            else\n            {\n                output.openPort(rt_midi_device);\n                ELKLOG_LOG_INFO(\"Midi output {} connected to {}\", sushi_midi_port, output.getPortName(rt_midi_device));\n            }\n        }\n        catch(RtMidiError& error)\n        {\n            ELKLOG_LOG_WARNING(\"Failed to connect midi output {} to RtMidi device with index {}: {}\", sushi_midi_port, rt_midi_device, error.getMessage());\n            return false;\n        }\n    }\n\n    return true;\n}\n\nvoid RtMidiFrontend::run()\n{\n\n}\n\nvoid RtMidiFrontend::stop()\n{\n    for (auto& input : _input_midi_ports)\n    {\n        input.midi_input.closePort();\n    }\n\n    for (auto& output : _output_midi_ports)\n    {\n        output.closePort();\n    }\n}\n\nvoid RtMidiFrontend::send_midi(int input, MidiDataByte data, [[maybe_unused]]Time timestamp)\n{\n    // Ignoring sysex for now\n    std::vector<unsigned char> message(data.data(), data.data() + RTMIDI_MESSAGE_SIZE);\n    _output_midi_ports[input].sendMessage(&message);\n}\n\n} // end namespace sushi::internal::midi_frontend\n\n"
  },
  {
    "path": "src/control_frontends/rt_midi_frontend.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n * @brief RT midi frontend, provides a frontend for getting midi messages into the engine\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n *\n * This module provides a frontend for getting midi messages into the engine\n */\n\n#ifndef SUSHI_RT_MIDI_FRONTEND_H\n#define SUSHI_RT_MIDI_FRONTEND_H\n\n#include <map>\n\n#include <rtmidi/RtMidi.h>\n\n#include \"base_midi_frontend.h\"\n\nnamespace sushi::internal::midi_frontend {\n\nstruct RtMidiCallbackData\n{\n    RtMidiIn midi_input;\n    int input_number;\n    midi_receiver::MidiReceiver* receiver;\n};\n\nclass RtMidiFrontend : public BaseMidiFrontend\n{\npublic:\n    explicit RtMidiFrontend(int inputs,\n                            int outputs,\n                            std::vector<std::tuple<int, int, bool>> input_mappings,\n                            std::vector<std::tuple<int, int, bool>> output_mappings,\n                            midi_receiver::MidiReceiver* receiver);\n\n    ~RtMidiFrontend();\n\n    bool init() override;\n\n    void run() override;\n\n    void stop() override;\n\n    void send_midi(int input, MidiDataByte data, Time timestamp) override;\n\nprivate:\n\n    int _inputs;\n    int _outputs;\n    std::vector<std::tuple<int, int, bool>> _input_mappings;\n    std::vector<std::tuple<int, int, bool>> _output_mappings;\n    std::vector<RtMidiCallbackData> _input_midi_ports;\n    std::vector<RtMidiOut> _output_midi_ports;\n};\n\n} // end namespace sushi::internal::midi_frontend\n\n#endif // SUSHI_RT_MIDI_FRONTEND_H\n"
  },
  {
    "path": "src/dsp_library/biquad_filter.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Biquad filter implementation\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"biquad_filter.h\"\n\n#if ! defined(_MSC_VER)\n#define _USE_MATH_DEFINES\n#endif\n\n#include <cmath>\n#include <algorithm>\n#include <numbers>\n\nnamespace sushi::dsp::biquad {\n\nconst int TIME_CONSTANTS_IN_SMOOTHING_FILTER = 3;\n\ninline float process_one_pole(const OnePoleCoefficients coefficients, const float input, float &z)\n{\n    z = coefficients.b0 * input + coefficients.a0 * z;\n    return z;\n}\n\nvoid calc_biquad_peak(Coefficients& filter, float samplerate, float frequency, float q, float gain)\n{\n    double A = std::sqrt(gain); // Note that the dB to linear gain conversion is done in the parameters preprocessor\n    double w0 = 2.0 * std::numbers::pi * frequency / samplerate;\n    double w0_cos = std::cos(w0);\n    double w0_sin = std::sin(w0);\n    double alpha = 0.5 * w0_sin / q;\n    double a0 = 1 + alpha / A;\n\n    // Calculating normalized filter coefficients\n    filter.a1 = static_cast<float>(-2.0f * w0_cos / a0);\n    filter.a2 = static_cast<float>((1 - alpha / A) / a0);\n    filter.b0 = static_cast<float>((1 + alpha * A) /a0);\n    filter.b1 = filter.a1;\n    filter.b2 = static_cast<float>((1 - alpha * A) / a0);\n}\n\nvoid calc_biquad_lowpass(Coefficients&  filter, float samplerate, float frequency)\n{\n    float w0 = 2.0f * std::numbers::pi_v<float> * frequency / samplerate;\n    float w0_cos = std::cos(w0);\n    float w0_sin = std::sin(w0);\n    float alpha = w0_sin;\n    float a0 = 1 + alpha;\n\n    // Calculating normalized filter coefficients\n    filter.a1 = -2.0f * w0_cos / a0;\n    filter.a2 = (1 - alpha) / a0;\n    filter.b0 = (1 - w0_cos) * 2.0f / a0;\n    filter.b1 = (1- w0_cos) / a0;\n    filter.b2 = filter.b0;\n}\n\nBiquadFilter::BiquadFilter()\n{\n}\n\nBiquadFilter::BiquadFilter(const Coefficients &coefficients) :\n        _coefficient_targets(coefficients)\n{\n}\n\nvoid BiquadFilter::reset()\n{\n    /* Clear everything that is time-dependant in the filters processing to\n     * put the filter in a default state */\n    _delay_registers = {0.0, 0.0};\n    _coefficients = _coefficient_targets;\n    std::fill(_smoothing_registers, _smoothing_registers + NUMBER_OF_BIQUAD_COEF, 0.0f);\n}\n\nvoid BiquadFilter::set_smoothing(int buffer_size)\n{\n    /* Coefficient changes are smoothed through a one pole lowpass filter\n     * with a time constant set to match a fixed number of samples\n     * Since the frequency low and the actual cut off frequency not crucial,\n     * we can get by without a bilinear transformation and simply\n     * calculate a time constant from an analogue prototype filter instead. */\n\n    _smoothing_coefficients.b0 = std::exp(-2.0f * std::numbers::pi_v<float> * (1.0f / static_cast<float>(buffer_size)) * TIME_CONSTANTS_IN_SMOOTHING_FILTER);\n    _smoothing_coefficients.a0 = 1 - _smoothing_coefficients.b0;\n}\n\nvoid BiquadFilter::set_coefficients(const Coefficients &coefficients)\n{\n    _coefficient_targets = coefficients;\n}\n\nvoid BiquadFilter::process(const float *input, float *output, int samples)\n{\n    for (int n = 0; n < samples; n++)\n    {\n        // Process the coefficients through a one pole smoothing filter\n        _coefficients.b0 = process_one_pole(_smoothing_coefficients, _coefficient_targets.b0, _smoothing_registers[0]);\n        _coefficients.b1 = process_one_pole(_smoothing_coefficients, _coefficient_targets.b1, _smoothing_registers[1]);\n        _coefficients.b2 = process_one_pole(_smoothing_coefficients, _coefficient_targets.b2, _smoothing_registers[2]);\n        _coefficients.a1 = process_one_pole(_smoothing_coefficients, _coefficient_targets.a1, _smoothing_registers[3]);\n        _coefficients.a2 = process_one_pole(_smoothing_coefficients, _coefficient_targets.a2, _smoothing_registers[4]);\n\n        // process actual filter data\n        float x = input[n];\n        float y = _coefficients.b0 * x + _delay_registers.z1;\n        _delay_registers.z1 = _coefficients.b1 * x - _coefficients.a1 * y + _delay_registers.z2;\n        _delay_registers.z2 = _coefficients.b2 * x - _coefficients.a2 * y;\n        output[n] = y;\n    }\n}\n\n} // end namespace sushi::dsp::biquad"
  },
  {
    "path": "src/dsp_library/biquad_filter.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Biquad filter implementation\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n *\n * A general biquad filter implementation with coefficient smoothing\n */\n\n#ifndef EQUALIZER_BIQUADFILTER_H\n#define EQUALIZER_BIQUADFILTER_H\n\nnamespace sushi::dsp::biquad {\n\nconst int NUMBER_OF_BIQUAD_COEF = 5;\n\nstruct Coefficients\n{\n    float b0;\n    float b1;\n    float b2;\n    float a1;\n    float a2;\n};\n\nstruct DelayRegisters\n{\n    float z1;\n    float z2;\n};\n\nstruct OnePoleCoefficients\n{\n    float b0;\n    float a0;\n};\n\n\n/*\n * Stand-alone functions for calculating coefficients\n */\nvoid calc_biquad_peak(Coefficients &filter, float samplerate, float frequency, float q, float gain);\n\nvoid calc_biquad_lowpass(Coefficients* filter, float samplerate, float frequency);\n\n/*\n * Filter class\n */\nclass BiquadFilter\n{\npublic:\n    BiquadFilter();\n\n    BiquadFilter(const Coefficients &coefficients);\n\n    ~BiquadFilter() = default;\n\n    /*\n     * Resets the processing state\n     */\n    void reset();\n\n    /*\n     * Sets the parameters for smoothing filter changes\n     */\n    void set_smoothing(int buffer_size);\n\n    void set_coefficients(const Coefficients &coefficients);\n\n    void process(const float* input, float* output, int samples);\n\nprivate:\n    Coefficients _coefficients{0.0f, 0.0f, 0.0f, 0.0f, 0.0f};\n    Coefficients _coefficient_targets{0.0f, 0.0f, 0.0f, 0.0f, 0.0f};\n    DelayRegisters _delay_registers{0.0f, 0.0f};\n    OnePoleCoefficients _smoothing_coefficients{0.0f, 0.0f};\n    float _smoothing_registers[NUMBER_OF_BIQUAD_COEF]{0.0f, 0.0f, 0.0f, 0.0f, 0.0f};\n};\n\n} // end namespace sushi::dsp::biquad\n\n#endif //EQUALIZER_BIQUADFILTER_H\n"
  },
  {
    "path": "src/dsp_library/envelopes.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Generic envelope classes to be used as building blocks for audio processors\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_ENVELOPES_H\n#define SUSHI_ENVELOPES_H\n\n#include \"sushi/constants.h\"\n\n// VST3SDK defines RELEASE globally, which leaks into all including code\n// And conflict with the RELEASE phase of the Adsr enum.\n#undef RELEASE\n\nnamespace sushi::dsp {\n\n/**\n * @brief Too avoid divisions by zero and extensive branching, we limit the\n * attack, decay and release times to some extremely short value and not 0.\n */\nconstexpr float SHORTEST_ENVELOPE_TIME = 1.0e-5f;\n\n\n/**\n * @brief A basic, linear slope, ADSR envelope class.\n */\nclass AdsrEnvelope\n{\n    SUSHI_DECLARE_NON_COPYABLE(AdsrEnvelope);\n\n    enum class EnvelopeState\n    {\n        OFF,\n        ATTACK,\n        DECAY,\n        SUSTAIN,\n        RELEASE,\n    };\n\npublic:\n    AdsrEnvelope() = default;\n\n    /**\n     * @brief Set the envelope parameters.\n     * @param attack Attack time in seconds.\n     * @param decay Decay time in seconds.\n     * @param sustain Sustain level, 0 - 1.\n     * @param release Release time in seconds.\n     */\n    void set_parameters(float attack, float decay, float sustain, float release)\n    {\n        if (attack < SHORTEST_ENVELOPE_TIME) attack = SHORTEST_ENVELOPE_TIME;\n        if (decay < SHORTEST_ENVELOPE_TIME) decay = SHORTEST_ENVELOPE_TIME;\n        if (release < SHORTEST_ENVELOPE_TIME) release = SHORTEST_ENVELOPE_TIME;\n\n        _attack_factor = 1 / (_samplerate * attack);\n        _decay_factor = (1.0f - sustain) / (_samplerate * decay);\n        _sustain_level = sustain;\n        _release_factor = sustain / (_samplerate * release);\n    }\n\n    /**\n     * @brief Set the current samplerate\n     * @param samplerate The samplerate in samples/second.\n     */\n    void set_samplerate(float samplerate)\n    { _samplerate = samplerate; }\n\n    /**\n     * @brief Advance the envelope a given number of samples and return\n     *        its current value.\n     * @param samples The number of samples to 'tick' the envelope.\n     * @return The current envelope level.\n     */\n    float tick(int samples = 0)\n    {\n        switch (_state)\n        {\n            case EnvelopeState::OFF:\n                break;\n\n            case EnvelopeState::ATTACK:\n                _current_level += static_cast<float>(samples) * _attack_factor;\n                if (_current_level >= 1)\n                {\n                    _state = EnvelopeState::DECAY;\n                    _current_level = 1.0f;\n                }\n                break;\n\n            case EnvelopeState::DECAY:\n                _current_level -= static_cast<float>(samples) * _decay_factor;\n                if (_current_level <= _sustain_level)\n                {\n                    _state = EnvelopeState::SUSTAIN;\n                    _current_level = _sustain_level;\n                }\n                break;\n\n            case EnvelopeState::SUSTAIN:\n                /* fixed level, wait for a gate release/note off */\n                break;\n\n            case EnvelopeState::RELEASE:\n                _current_level -= static_cast<float>(samples) * _release_factor;\n                if (_current_level < 0.0f)\n                {\n                    _state = EnvelopeState::OFF;\n                    _current_level = 0.0f;\n                }\n                break;\n        }\n        return _current_level;\n    }\n\n    /**\n     * @brief Get the envelopes current level without advancing it.\n     * @return The current envelope level.\n     */\n    [[nodiscard]] float level() const\n    {\n        return _current_level;\n    }\n\n    /**\n     * @brief Analogous to the gate signal on an analog envelope. Setting gate\n     *        to true will start the envelope in the attack phase and setting\n     *        it to false will start the release phase.\n     * @param gate Set to true for note on and false at note off.\n     */\n    void gate(bool gate)\n    {\n        if (gate) /* If the envelope is running, it's simply restarted here */\n        {\n            _state = EnvelopeState::ATTACK;\n            _current_level = 0.0f;\n        } else /* Gate off - go to release phase */\n        {\n            /* if the envelope enters the release phase from the attack or decay\n             * phase, the release factor must be rescaled to give the correct slope */\n            if (_state != EnvelopeState::SUSTAIN && _sustain_level > 0)\n            {\n                _release_factor *= (_current_level / _sustain_level);\n            }\n            _state = EnvelopeState::RELEASE;\n        }\n    }\n\n    /**\n     * @brief Returns true if the envelope is off, ie. the release phase is finished.\n     * @return\n     */\n    bool finished()\n    { return _state == EnvelopeState::OFF; }\n\n    /**\n     * @brief Resets the envelope to 0 immediately. Bypassing any long release\n     *        phase.\n     */\n    void reset()\n    {\n        _state = EnvelopeState::OFF;\n        _current_level = 0.0f;\n    }\n\nprivate:\n    float _attack_factor {0};\n    float _decay_factor {0};\n    float _sustain_level {1};\n    float _release_factor {0.1f};\n    float _current_level {0};\n    float _samplerate {44100};\n\n    EnvelopeState _state {EnvelopeState::OFF};\n};\n\n} // end namespace sushi::dsp\n\n#endif // SUSHI_ENVELOPES_H\n"
  },
  {
    "path": "src/dsp_library/master_limiter.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Class to be used for hard limitng audio signals\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_MASTER_LIMITER_H\n#define SUSHI_MASTER_LIMITER_H\n\n#include <array>\n#include <cmath>\n\nnamespace sushi::dsp {\n\n/**\n * Calculated using windowed sinc method. Basic tests in python\n * seemed to get good results with low latency. Might not be sufficient to\n * listen to but works for true peak detection\n */\n\nconstexpr float filter_coeffs[4][4] = {\n    { -0.06615947186946869, 0.19239433109760284, 0.9733920693397522, -1.6899518229251953e-08,},\n    { -0.09243691712617874, 0.4796152412891388, 0.779610812664032, -0.08357855677604675,},\n    { -0.08357856422662735, 0.779610812664032, 0.4796152114868164, -0.09243690967559814,},\n    { -1.6899520005608792e-08, 0.973392128944397, 0.19239431619644165, -0.06615947186946869,}\n};\n\nconstexpr float THRESHOLD_DB = 0.0;\nconstexpr float THRESHOLD_GAIN = 1.0;\nconstexpr float RELEASE_TIME_MS = 100.0;\nconstexpr float ATTACK_TIME_MS = 0.0;\nconstexpr int UPSAMPLING_FACTOR = 4;\n/**\n * Since exponentials never reach their target this constant is used\n * to set a higher target than the intended one. This is then reversed\n * when checking if we reached the correct level\n *\n * Experiments in numpy showed 1.6 has good correlation with the attack time for a range of settings\n *\n */\nconstexpr float ATTACK_RATIO = 1.6f;\n/**\n * @brief 4x polyphase interpolator.\n *\n */\ntemplate<int CHUNK_SIZE>\nclass UpSampler\n{\npublic:\n    UpSampler() {}\n\n    /**\n     * @brief Reset the interpolator for when processing is about to start\n     *\n     */\n    void reset()\n    {\n        _delay_line.fill(0.0f);\n    }\n\n    /**\n     * @brief Interpolate one sample to 4x the original sample_rate\n     * using a polyphase implementation\n     *\n     * @param sample The sample to interpolate\n     * @return std::array<float, 4> Resulting samples\n     */\n    inline void process(const float* input, float* output)\n    {\n        for (int sample_idx = 0; sample_idx < CHUNK_SIZE; sample_idx++)\n        {\n            _delay_line[_write_idx] = input[sample_idx]; // Write sample into internal delayline\n            for (size_t i = 0; i < UPSAMPLING_FACTOR; i++)\n            {\n                float upsampled_value = 0.0;\n                // Convolve the filter with the sample data\n                for (size_t j = 0; j < _delay_line.size(); j++)\n                {\n                    int read_idx = (_write_idx - j) & 0b11;\n                    upsampled_value += filter_coeffs[i][j] * _delay_line[read_idx];\n                }\n                output[UPSAMPLING_FACTOR * sample_idx + i] = upsampled_value;\n            }\n            _write_idx = (_write_idx + 1) & 0b11; // Fast index wrapping for 2^n size circular buffers\n        }\n    }\nprivate:\n    std::array<float, 4> _delay_line;\n    int _write_idx{0};\n};\n\n/**\n * @brief Brick wall \"ear-saving\" limiter. Stops the signal from ever exceeding 0.0 dB.\n * Instant attack with true peak detection. Could cause distortion in the \"attack\" portion\n * of a signal.\n *\n */\ntemplate<int CHUNK_SIZE>\nclass MasterLimiter\n{\npublic:\n\n    MasterLimiter(float release_time_ms = RELEASE_TIME_MS,\n                  float attack_time_ms = ATTACK_TIME_MS) : _release_time(release_time_ms),\n                                                           _attack_time(attack_time_ms) {}\n\n    /**\n     * @brief Recalculate release time based on sample rate and reset gain reduction\n     * and up sampler.\n     *\n     * @param sample_rate\n     */\n    void init(float sample_rate)\n    {\n        _release_coeff = _release_time > 0.0f ? std::exp(-1.0f / (0.001f * sample_rate * _release_time)) : 0.0f;\n        _attack_coeff = _attack_time > 0.0f ? std::exp(-1.0f / (0.001f * sample_rate * _attack_time)) : 0.0f;\n        _gain_reduction = 0.0f;\n        _up_sampler.reset();\n    }\n\n    /**\n     * @brief Process audio limiting to output to maximum 0.0 dB\n     *\n     * @param input array of input values\n     * @param output array of output values\n     * @param n_samples number of samples to process\n     */\n    void process(const float* input, float* output)\n    {\n        std::array<float, UPSAMPLING_FACTOR * CHUNK_SIZE> up_sampled_values;\n        _up_sampler.process(input, up_sampled_values.data());\n        for (int sample_idx = 0; sample_idx < CHUNK_SIZE; sample_idx++)\n        {\n            // Calculate the highest peak from true peak calculations and the current sample value\n            float true_peak = std::abs(input[sample_idx]);\n            for (int upsampled_idx = 0; upsampled_idx < UPSAMPLING_FACTOR; upsampled_idx++)\n            {\n                true_peak = std::max(true_peak, std::abs(up_sampled_values[UPSAMPLING_FACTOR * sample_idx + upsampled_idx]));\n            }\n\n            // Calculate gain reduction\n            if (true_peak > THRESHOLD_GAIN)\n            {\n                _gain_reduction_target = std::max(_gain_reduction_target, (1.0f - 1.0f / true_peak) * ATTACK_RATIO);\n            }\n\n            if (_gain_reduction_target > _gain_reduction)\n            {\n                _gain_reduction = (_gain_reduction - _gain_reduction_target) * _attack_coeff + _gain_reduction_target;\n                if (_gain_reduction >= _gain_reduction_target / ATTACK_RATIO)\n                {\n                    _gain_reduction_target = 0.0;\n                }\n            }\n            else\n            {\n                _gain_reduction *= _release_coeff;\n            }\n            output[sample_idx] = input[sample_idx] * (1.0f - _gain_reduction);\n        }\n    }\n\nprivate:\n    float _gain_reduction{0.0};\n    float _gain_reduction_target{0.0};\n    float _release_time{0.0};\n    float _release_coeff{0.0};\n    float _attack_time{0.0};\n    float _attack_coeff{0.0};\n\n    UpSampler<CHUNK_SIZE> _up_sampler;\n};\n\n} // end namespace sushi::dsp\n\n\n#endif // SUSHI_MASTER_LIMITER_H\n"
  },
  {
    "path": "src/dsp_library/sample_wrapper.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Wrapper for an audio sample to provide sample interpolation in an OO interface\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_AUDIO_SAMPLE_H\n#define SUSHI_AUDIO_SAMPLE_H\n\n#include <cmath>\n\nnamespace sushi::dsp {\n\n/**\n * @brief Class to wrap a mono audio sample into a prettier interface\n */\nclass Sample\n{\n    SUSHI_DECLARE_NON_COPYABLE(Sample);\npublic:\n    Sample() = default;\n\n    Sample(const float* sample, int length) : _data(sample), _length(length) {}\n\n    /**\n     * @brief Set the sample to wrap.\n     * @param sample Pointer to sample array, Sample does not take ownership of the data.\n     * @param length Number of samples in the data.\n     */\n    void set_sample(const float* sample_data, int length)\n    {\n        _data = sample_data;\n        _length = length;\n    }\n\n    /**\n     * @brief Return the value at sample position. Does linear interpolation\n     * @param position The position in the sample buffer.\n     * @return A linearly interpolated sample value.\n     */\n    [[nodiscard]] float at(double position) const\n    {\n        assert(position >= 0);\n        assert(_data);\n\n        int sample_pos = static_cast<int>(position);\n        auto weight = static_cast<float>(position - std::floor(position));\n        float sample_low = (sample_pos < _length) ? _data[sample_pos] : 0.0f;\n        float sample_high = (sample_pos + 1 < _length) ? _data[sample_pos + 1] : 0.0f;\n\n        return (sample_high * weight + sample_low * (1.0f - weight));\n    }\n\nprivate:\n    const float* _data{nullptr};\n    int _length{0};\n};\n\n} // end namespace sushi::dsp\n#endif // SUSHI_AUDIO_SAMPLE_H\n"
  },
  {
    "path": "src/dsp_library/value_smoother.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Utility class for smoothing parameters or other time dependent values\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_VALUE_SMOOTHER_H\n#define SUSHI_VALUE_SMOOTHER_H\n\n#include <chrono>\n#include <cmath>\n#include <cassert>\n\nnamespace sushi::internal {\n\n/**\n * @brief Class that implements smoothing of a value over a set time period using either\n *        linear ramping, filtering through a 1 pole lowpass filter or an exponential ramp\n *        that is specifically made for with long audio fades as it has an exponential\n *        curve when fading in or out.\n *        The time set with set_lag_time() represents the actual ramping time in the linear ramp\n *        and volume case, and the 90% rise time in the filter case.\n *        The base template is not intended to be used directly but through one of the\n *        aliases ValueSmootherFilter or ValueSmootherRamp.\n * @tparam T The type used for the stored value.\n * @tparam mode The mode of filtering used. Should be either RAMP, FILTER or EXP_RAMP\n */\ntemplate <typename T, int mode>\nclass ValueSmoother\n{\npublic:\n    enum Mode : int\n    {\n        RAMP = 0,\n        EXP_RAMP,\n        FILTER\n    };\n    ValueSmoother() : _current_value(0), _target_value(0) {}\n\n    ValueSmoother(std::chrono::microseconds lag_time, float sample_rate) : _current_value(0),\n                                                                           _target_value(0)\n    {\n        _update_internals(lag_time, sample_rate);\n    }\n\n    ValueSmoother(std::chrono::microseconds lag_time, float sample_rate, T init_value) : _current_value(init_value),\n                                                                                         _target_value(init_value)\n    {\n        _update_internals(lag_time, sample_rate);\n    }\n\n    ~ValueSmoother() = default;\n\n    /**\n     * @brief Set the desired value\n     * @param value The new value to set\n     */\n    void set(T value)\n    {\n        if (value != _target_value)\n        {\n            _target_value = value;\n            if constexpr (mode == Mode::RAMP)\n            {\n                _spec.step = std::min(1.0f, (_target_value - _current_value) / _spec.steps);\n                _spec.count = _spec.steps;\n            }\n            else if constexpr (mode == Mode::EXP_RAMP)\n            {\n                _spec.count = _spec.steps;\n                _spec.step = std::exp((std::log(std::max(STATIONARY_LIMIT, value)) -\n                                       std::log(std::max(STATIONARY_LIMIT, _current_value))) / _spec.steps);\n            }\n        }\n    };\n\n    /**\n     * @brief Set the desired value directly without applying any smoothing\n     * @param value The new value to set\n     */\n    void set_direct(T target_value)\n    {\n        _target_value = target_value;\n        if constexpr (mode == Mode::EXP_RAMP)\n        {\n            _current_value = std::max(STATIONARY_LIMIT, target_value);\n        }\n        else\n        {\n            _current_value = target_value;\n        }\n\n        if constexpr (mode == Mode::RAMP)\n        {\n            _spec.count = 0;\n        }\n    }\n\n    /**\n     * @brief Read the current value without updating the object\n     * @return The current smoothed value\n     */\n    [[nodiscard]] T value() const  {return _current_value;}\n\n    /**\n     * @brief Advance the smoother one sample point and return the new current value\n     * @return The current smoothed value\n     */\n    T next_value()\n    {\n        if constexpr (mode == Mode::RAMP || mode == Mode::EXP_RAMP)\n        {\n            assert(_spec.steps >= 0);\n            if (_spec.count > 0)\n            {\n                _spec.count--;\n                if constexpr (mode == Mode::RAMP)\n                {\n                    return _current_value += _spec.step;\n                }\n                else\n                {\n                    return _current_value *= _spec.step;\n                }\n            }\n            return _target_value;\n        }\n        else\n        {\n            assert(_spec.coeff != 0);\n\n            _current_value = (static_cast<T>(1.0) - _spec.coeff) * _target_value + _spec.coeff * _current_value;\n\n            return _current_value;\n        }\n    }\n\n    /**\n     * @brief Test whether the smoother has reached the target value.\n     * @return true if the value has reached the target value, false otherwise\n     */\n    [[nodiscard]] bool stationary() const\n    {\n        if constexpr (mode == Mode::RAMP || mode == Mode::EXP_RAMP)\n        {\n            return _spec.count == 0;\n        }\n        else\n        {\n            return std::abs(_target_value - _current_value) < STATIONARY_LIMIT;\n        }\n    }\n\n    /**\n     * @brief Set the smoothing parameters\n     * @param lag_time The approximate time to reach the target value\n     * @param sample_rate The rate at which the smoother is updated\n     */\n    void set_lag_time(std::chrono::duration<float, std::ratio<1,1>> lag_time, float sample_rate)\n    {\n        _update_internals(lag_time, sample_rate);\n    }\n\nprivate:\n    struct LinearSpecific\n    {\n        T   step{0};\n        int count{0};\n        int steps{0};\n    };\n    struct FilterSpecific\n    {\n        T   coeff{0};\n    };\n\n    static constexpr T TIMECONSTANTS_RISE_TIME = static_cast<const T>(2.19);\n    static constexpr T STATIONARY_LIMIT = static_cast<const T>(0.0001); // -80dB\n\n    static_assert(mode == Mode::RAMP || mode == Mode::FILTER || mode == Mode::EXP_RAMP);\n\n    void _update_internals(std::chrono::duration<float, std::ratio<1,1>> lag_time, float sample_rate)\n    {\n        if constexpr (mode == Mode::FILTER)\n        {\n            _spec.coeff = std::exp(static_cast<T>(-1.0 * TIMECONSTANTS_RISE_TIME) / (lag_time.count() * sample_rate));\n        }\n        else\n        {\n            if constexpr (mode == Mode::EXP_RAMP)\n            {\n                _current_value = std::max(_current_value, STATIONARY_LIMIT);\n            }\n            _spec.steps = std::max(1, static_cast<int>(std::round(lag_time.count() * sample_rate)));\n        }\n    }\n\n    T _current_value;\n    T _target_value;\n    std::conditional_t<mode == Mode::RAMP || mode == Mode::EXP_RAMP, LinearSpecific, FilterSpecific> _spec;\n};\n\n/* ValueSmoother should be declared through one of the aliases below */\ntemplate <typename FloatType>\nusing ValueSmootherRamp = ValueSmoother<FloatType, ValueSmoother<FloatType,0>::Mode::RAMP>;\n\ntemplate <typename FloatType>\nusing ValueSmootherFilter = ValueSmoother<FloatType, ValueSmoother<FloatType,0>::Mode::FILTER>;\n\ntemplate <typename FloatType>\nusing ValueSmootherExpRamp = ValueSmoother<FloatType, ValueSmoother<FloatType,0>::Mode::EXP_RAMP>;\n\n}  // end namespace sushi::internal\n\n#endif // SUSHI_VALUE_SMOOTHER_H\n\n"
  },
  {
    "path": "src/engine/audio_engine.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Real time audio processing engine\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <fstream>\n#include <iomanip>\n#include <functional>\n\n#define TWINE_EXPOSE_INTERNALS\n#include \"twine/twine.h\"\n#include \"elklog/static_logger.h\"\n\n#include \"audio_engine.h\"\n\n\nnamespace sushi::internal::engine {\n\nconstexpr auto CLIPPING_DETECTION_INTERVAL = std::chrono::milliseconds(500);\n\n#ifdef _MSC_VER\nconstexpr auto RT_EVENT_TIMEOUT = std::chrono::milliseconds(500);\n#else\nconstexpr auto RT_EVENT_TIMEOUT = std::chrono::milliseconds(200);\n#endif\n\nconstexpr char TIMING_FILE_NAME[] = \"timings.txt\";\nconstexpr int  TIMING_LOG_PRINT_INTERVAL = 15;\n\nconstexpr int  MAX_TRACKS = 32;\nconstexpr int  MAX_AUDIO_CONNECTIONS = MAX_TRACKS * MAX_TRACK_CHANNELS;\nconstexpr int  MAX_CV_CONNECTIONS = MAX_ENGINE_CV_IO_PORTS * 10;\nconstexpr int  MAX_GATE_CONNECTIONS = MAX_ENGINE_GATE_PORTS * 10;\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"engine\");\n\nEngineReturnStatus to_engine_status(ProcessorReturnCode processor_status)\n{\n    switch (processor_status)\n    {\n        case ProcessorReturnCode::OK:                       return EngineReturnStatus::OK;\n        case ProcessorReturnCode::ERROR:                    return EngineReturnStatus::ERROR;\n        case ProcessorReturnCode::PARAMETER_ERROR:          return EngineReturnStatus::INVALID_PARAMETER;\n        case ProcessorReturnCode::PARAMETER_NOT_FOUND:      return EngineReturnStatus::INVALID_PARAMETER;\n        case ProcessorReturnCode::UNSUPPORTED_OPERATION:    return EngineReturnStatus::INVALID_PLUGIN_TYPE;\n        default:                                            return EngineReturnStatus::ERROR;\n    }\n}\n\nvoid ClipDetector::set_sample_rate(float sample_rate)\n{\n    _interval = static_cast<unsigned int>(sample_rate * CLIPPING_DETECTION_INTERVAL.count() / 1000 - AUDIO_CHUNK_SIZE);\n}\n\nvoid ClipDetector::set_channels(int inputs, int outputs)\n{\n    _input_clip_count = std::vector<unsigned int>(inputs, _interval);\n    _output_clip_count = std::vector<unsigned int>(outputs, _interval);\n}\n\nvoid ClipDetector::detect_clipped_samples(const ChunkSampleBuffer& buffer, RtSafeRtEventFifo& queue, bool audio_input)\n{\n    auto& counter = audio_input? _input_clip_count : _output_clip_count;\n    for (int i = 0; i < buffer.channel_count(); ++i)\n    {\n        if (buffer.count_clipped_samples(i) > 0 && counter[i] >= _interval)\n        {\n            queue.push(RtEvent::make_clip_notification_event(0, i, audio_input? ClipNotificationRtEvent::ClipChannelType::INPUT:\n                                                                   ClipNotificationRtEvent::ClipChannelType::OUTPUT));\n            counter[i] = 0;\n        }\n        else\n        {\n            counter[i] += AUDIO_CHUNK_SIZE;\n        }\n    }\n}\n\n\nAudioEngine::AudioEngine(float sample_rate,\n                         int rt_threads,\n                         std::optional<std::string> device_name,\n                         bool debug_mode_sw,\n                         dispatcher::BaseEventDispatcher* event_dispatcher) : BaseEngine::BaseEngine(sample_rate),\n                                                          _audio_graph(rt_threads, MAX_TRACKS, &_process_timer, sample_rate, device_name, debug_mode_sw),\n                                                          _audio_in_connections(MAX_AUDIO_CONNECTIONS),\n                                                          _audio_out_connections(MAX_AUDIO_CONNECTIONS),\n                                                          _transport(sample_rate, &_main_out_queue),\n                                                          _clip_detector(sample_rate)\n{\n    if (event_dispatcher == nullptr)\n    {\n        _event_dispatcher = std::make_unique<dispatcher::EventDispatcher>(this, &_main_out_queue, &_main_in_queue);\n    }\n    else\n    {\n        _event_dispatcher.reset(event_dispatcher);\n    }\n    _host_control = HostControl(_event_dispatcher.get(), &_transport, &_plugin_library);\n\n    this->set_sample_rate(sample_rate);\n    _cv_in_connections.reserve(MAX_CV_CONNECTIONS);\n    _gate_in_connections.reserve(MAX_GATE_CONNECTIONS);\n}\n\nAudioEngine::~AudioEngine()\n{\n    _event_dispatcher->stop();\n    if (_process_timer.enabled())\n    {\n        _process_timer.enable(false);\n        print_timings_to_file(TIMING_FILE_NAME);\n    }\n}\n\nvoid AudioEngine::set_sample_rate(float sample_rate)\n{\n    BaseEngine::set_sample_rate(sample_rate);\n    for (auto& node : _processors.all_processors())\n    {\n        _processors.mutable_processor(node->id())->configure(sample_rate);\n    }\n    _transport.set_sample_rate(sample_rate);\n    _process_timer.set_timing_period(sample_rate, AUDIO_CHUNK_SIZE);\n    _clip_detector.set_sample_rate(sample_rate);\n    for (auto& limiter : _master_limiters)\n    {\n        limiter.init(sample_rate);\n    }\n}\n\nvoid AudioEngine::set_audio_channels(int inputs, int outputs)\n{\n    _clip_detector.set_channels(inputs, outputs);\n\n    BaseEngine::set_audio_channels(inputs, outputs);\n    _input_swap_buffer = ChunkSampleBuffer(inputs);\n    _output_swap_buffer = ChunkSampleBuffer(outputs);\n\n    _master_limiters.clear();\n    for (int c = 0; c < outputs; c++)\n    {\n        _master_limiters.emplace_back();\n    }\n}\n\nEngineReturnStatus AudioEngine::set_cv_input_channels(int channels)\n{\n    if (channels > MAX_ENGINE_CV_IO_PORTS)\n    {\n        return EngineReturnStatus::INVALID_N_CHANNELS;\n    }\n    return BaseEngine::set_cv_input_channels(channels);\n}\n\nEngineReturnStatus AudioEngine::set_cv_output_channels(int channels)\n{\n    if (channels > MAX_ENGINE_CV_IO_PORTS)\n    {\n        return EngineReturnStatus::INVALID_N_CHANNELS;\n    }\n    return BaseEngine::set_cv_output_channels(channels);\n}\n\nEngineReturnStatus AudioEngine::connect_audio_input_channel(int input_channel, int track_channel, ObjectId track_id)\n{\n    return _connect_audio_channel(input_channel, track_channel, track_id, Direction::INPUT);\n}\n\nEngineReturnStatus AudioEngine::connect_audio_output_channel(int output_channel, int track_channel, ObjectId track_id)\n{\n    return _connect_audio_channel(output_channel, track_channel, track_id, Direction::OUTPUT);\n}\n\nEngineReturnStatus AudioEngine::disconnect_audio_input_channel(int engine_channel, int track_channel, ObjectId track_name)\n{\n    return _disconnect_audio_channel(engine_channel, track_channel, track_name, Direction::INPUT);\n}\n\nEngineReturnStatus AudioEngine::disconnect_audio_output_channel(int engine_channel, int track_channel, ObjectId track_name)\n{\n    return _disconnect_audio_channel(engine_channel, track_channel, track_name, Direction::OUTPUT);\n}\n\nstd::vector<AudioConnection> AudioEngine::audio_input_connections()\n{\n    return _audio_in_connections.connections();\n}\n\nstd::vector<AudioConnection> AudioEngine::audio_output_connections()\n{\n    return _audio_out_connections.connections();\n}\n\nEngineReturnStatus AudioEngine::connect_audio_input_bus(int input_bus, int track_bus, ObjectId track_id)\n{\n    auto status = connect_audio_input_channel(input_bus * 2, track_bus * 2, track_id);\n    if (status != EngineReturnStatus::OK)\n    {\n        return status;\n    }\n    return connect_audio_input_channel(input_bus * 2 + 1, track_bus * 2 + 1, track_id);\n}\n\nEngineReturnStatus AudioEngine::connect_audio_output_bus(int output_bus, int track_bus, ObjectId track_id)\n{\n    auto status = connect_audio_output_channel(output_bus * 2, track_bus * 2, track_id);\n    if (status != EngineReturnStatus::OK)\n    {\n        return status;\n    }\n    return connect_audio_output_channel(output_bus * 2 + 1, track_bus * 2 + 1, track_id);\n}\n\nEngineReturnStatus AudioEngine::connect_cv_to_parameter(const std::string& processor_name,\n                                                        const std::string& parameter_name,\n                                                        int cv_input_id)\n{\n    if (cv_input_id >= _cv_inputs)\n    {\n        return EngineReturnStatus::INVALID_CHANNEL;\n    }\n    auto processor = _processors.mutable_processor(processor_name);\n    if (processor == nullptr)\n    {\n        return EngineReturnStatus::INVALID_PROCESSOR;\n    }\n    auto param = processor->parameter_from_name(parameter_name);\n    if (param == nullptr)\n    {\n        return EngineReturnStatus::INVALID_PARAMETER;\n    }\n    CvConnection con;\n    con.processor_id = processor->id();\n    con.parameter_id = param->id();\n    con.cv_id = cv_input_id;\n    _cv_in_connections.push_back(con);\n    ELKLOG_LOG_INFO(\"Connected cv input {} to parameter {} on {}\", cv_input_id, parameter_name, processor_name);\n    return EngineReturnStatus::OK;\n}\n\nEngineReturnStatus AudioEngine::connect_cv_from_parameter(const std::string& processor_name,\n                                                          const std::string& parameter_name,\n                                                          int cv_output_id)\n{\n    if (cv_output_id >= _cv_outputs)\n    {\n        return EngineReturnStatus::ERROR;\n    }\n    auto processor = _processors.mutable_processor(processor_name);\n    if (processor == nullptr)\n    {\n        return EngineReturnStatus::INVALID_PROCESSOR;\n    }\n    auto param = processor->parameter_from_name(parameter_name);\n    if (param == nullptr)\n    {\n        return EngineReturnStatus::INVALID_PARAMETER;\n    }\n    auto res = processor->connect_cv_from_parameter(param->id(), cv_output_id);\n    if (res != ProcessorReturnCode::OK)\n    {\n        return EngineReturnStatus::ERROR;\n    }\n    ELKLOG_LOG_INFO(\"Connected parameter {} on {} to cv output {}\", parameter_name, processor_name, cv_output_id);\n    return EngineReturnStatus::OK;\n}\n\nEngineReturnStatus AudioEngine::connect_gate_to_processor(const std::string& processor_name,\n                                                          int gate_input_id,\n                                                          int note_no,\n                                                          int channel)\n{\n    if (gate_input_id >= MAX_ENGINE_GATE_PORTS || note_no > MAX_ENGINE_GATE_NOTE_NO)\n    {\n        return EngineReturnStatus::ERROR;\n    }\n    auto processor = _processors.mutable_processor(processor_name);\n    if (processor == nullptr)\n    {\n        return EngineReturnStatus::INVALID_PROCESSOR;\n    }\n    GateConnection con;\n    con.processor_id = processor->id();\n    con.note_no = note_no;\n    con.channel = channel;\n    con.gate_id = gate_input_id;\n    _gate_in_connections.push_back(con);\n    ELKLOG_LOG_INFO(\"Connected gate input {} to processor {} on channel {}\", gate_input_id, processor_name, channel);\n    return EngineReturnStatus::OK;\n}\n\nEngineReturnStatus AudioEngine::connect_gate_from_processor(const std::string& processor_name,\n                                                            int gate_output_id,\n                                                            int note_no,\n                                                            int channel)\n{\n    if (gate_output_id >= MAX_ENGINE_GATE_PORTS || note_no > MAX_ENGINE_GATE_NOTE_NO)\n    {\n        return EngineReturnStatus::ERROR;\n    }\n    auto processor = _processors.mutable_processor(processor_name);\n    if (processor == nullptr)\n    {\n        return EngineReturnStatus::INVALID_PROCESSOR;\n    }\n    auto res = processor->connect_gate_from_processor(gate_output_id, channel, note_no);\n    if (res != ProcessorReturnCode::OK)\n    {\n        return EngineReturnStatus::ERROR;\n    }\n    ELKLOG_LOG_INFO(\"Connected processor {} to gate output {} from channel {}\", gate_output_id, processor_name, channel);\n    return EngineReturnStatus::OK;\n}\n\nEngineReturnStatus AudioEngine::connect_gate_to_sync(int /*gate_input_id*/, int /*ppq_ticks*/)\n{\n    // TODO -  Needs implementing\n    return EngineReturnStatus::OK;\n}\n\nEngineReturnStatus AudioEngine::connect_sync_to_gate(int /*gate_output_id*/, int /*ppq_ticks*/)\n{\n    // TODO -  Needs implementing\n    return EngineReturnStatus::OK;\n}\n\nbool AudioEngine::realtime()\n{\n    return _state.load() != RealtimeState::STOPPED;\n}\n\nvoid AudioEngine::enable_realtime(bool enabled)\n{\n    if (enabled)\n    {\n        _state.store(RealtimeState::STARTING);\n    }\n    else\n    {\n        _state.store(RealtimeState::STOPPED);\n    }\n}\n\nEngineReturnStatus AudioEngine::_register_processor(std::shared_ptr<Processor> processor, const std::string& name)\n{\n    if (name.empty())\n    {\n        ELKLOG_LOG_ERROR(\"Plugin name is not specified\");\n        return EngineReturnStatus::INVALID_PLUGIN;\n    }\n    processor->set_name(name);\n    if (_processors.add_processor(processor) == false)\n    {\n        ELKLOG_LOG_WARNING(\"Processor with this name already exists\");\n        return EngineReturnStatus::INVALID_PROCESSOR;\n    }\n    ELKLOG_LOG_DEBUG(\"Successfully registered processor {}.\", name);\n    return EngineReturnStatus::OK;\n}\n\nvoid AudioEngine::_deregister_processor(Processor* processor)\n{\n    assert(processor);\n    assert(processor->active_rt_processing() == false);\n    _processors.remove_processor(processor->id());\n    ELKLOG_LOG_INFO(\"Successfully de-registered processor {}\", processor->name());\n}\n\nbool AudioEngine::_insert_processor_in_realtime_part(Processor* processor)\n{\n    if (processor->id() > _realtime_processors.size())\n    {\n        /* TODO - When non-rt callbacks for events are ready we can have the\n         * rt processor list re-allocated outside the rt domain\n         * In the meantime, put a limit on the number of processors */\n        ELKLOG_LOG_ERROR(\"Realtime processor list full\");\n        assert(false);\n    }\n    if (_realtime_processors[processor->id()] != nullptr)\n    {\n        return false;\n    }\n    _realtime_processors[processor->id()] = processor;\n    return true;\n}\n\nbool AudioEngine::_remove_processor_from_realtime_part(ObjectId processor)\n{\n    if (_realtime_processors[processor] == nullptr)\n    {\n        return false;\n    }\n    _realtime_processors[processor] = nullptr;\n    return true;\n}\n\nvoid AudioEngine::_remove_connections_from_track(ObjectId track_id)\n{\n    for (auto con : _audio_out_connections.connections())\n    {\n        if (con.track == track_id)\n        {\n            this->disconnect_audio_output_channel(con.engine_channel, con.track_channel, con.track);\n        }\n    }\n    for (auto con : _audio_in_connections.connections())\n    {\n        if (con.track == track_id)\n        {\n            this->disconnect_audio_input_channel(con.engine_channel, con.track_channel, con.track);\n        }\n    }\n}\n\nvoid AudioEngine::process_chunk(SampleBuffer<AUDIO_CHUNK_SIZE>* in_buffer,\n                                SampleBuffer<AUDIO_CHUNK_SIZE>* out_buffer,\n                                ControlBuffer* in_controls,\n                                ControlBuffer* out_controls,\n                                Time timestamp,\n                                int64_t sample_count)\n{\n    /* Signal that this is a realtime audio processing thread */\n    twine::ThreadRtFlag rt_flag;\n\n    auto engine_timestamp = _process_timer.start_timer();\n\n    _transport.set_time(timestamp, sample_count);\n\n    _process_internal_rt_events();\n    _send_rt_events_to_processors();\n\n    if (_cv_inputs > 0)\n    {\n        _route_cv_gate_ins(*in_controls);\n    }\n\n    _event_dispatcher->set_time(_transport.current_process_time());\n    auto state = _state.load();\n\n    if (_input_clip_detection_enabled)\n    {\n        _clip_detector.detect_clipped_samples(*in_buffer, _main_out_queue, true);\n    }\n\n    if (_pre_track)\n    {\n        _pre_track->process_audio(*in_buffer, _input_swap_buffer);\n        _copy_audio_to_tracks(&_input_swap_buffer);\n    }\n    else\n    {\n        _copy_audio_to_tracks(in_buffer);\n    }\n\n    // Render all tracks. If running in multicore mode, this part is processed in parallel.\n    _audio_graph.render();\n\n    _retrieve_events_from_tracks(*out_controls);\n    _main_out_queue.push(RtEvent::make_synchronisation_event(_transport.current_process_time()));\n    _state.store(update_state(state));\n\n    if (_post_track)\n    {\n        _copy_audio_from_tracks(&_output_swap_buffer);\n        _post_track->process_audio(_output_swap_buffer, *out_buffer);\n    }\n    else\n    {\n        _copy_audio_from_tracks(out_buffer);\n    }\n\n    if (_master_limiter_enabled)\n    {\n        auto temp_input_buffer = ChunkSampleBuffer::create_non_owning_buffer(*out_buffer, 0, out_buffer->channel_count());\n        for (int c = 0; c < out_buffer->channel_count(); c++)\n        {\n            _master_limiters[c].process(out_buffer->channel(c), out_buffer->channel(c));\n        }\n        out_buffer->replace(temp_input_buffer);\n    }\n\n    if (_output_clip_detection_enabled)\n    {\n        _clip_detector.detect_clipped_samples(*out_buffer, _main_out_queue, false);\n    }\n    _process_timer.stop_timer(engine_timestamp, ENGINE_TIMING_ID);\n}\n\nvoid AudioEngine::set_tempo(float tempo)\n{\n    bool realtime_running = _state != RealtimeState::STOPPED;\n    _transport.set_tempo(tempo, realtime_running);\n    if (realtime_running)\n    {\n        auto e = RtEvent::make_tempo_event(0, tempo);\n        _send_control_event(e);\n    }\n}\n\nvoid AudioEngine::set_time_signature(TimeSignature signature)\n{\n    bool realtime_running = _state != RealtimeState::STOPPED;\n    _transport.set_time_signature(signature, realtime_running);\n    if (realtime_running)\n    {\n        auto e = RtEvent::make_time_signature_event(0, signature);\n        _send_control_event(e);\n    }\n}\n\nvoid AudioEngine::set_transport_mode(PlayingMode mode)\n{\n    bool realtime_running = _state != RealtimeState::STOPPED;\n    _transport.set_playing_mode(mode, realtime_running);\n    if (realtime_running)\n    {\n        auto e = RtEvent::make_playing_mode_event(0, mode);\n        _send_control_event(e);\n    }\n}\n\nvoid AudioEngine::set_tempo_sync_mode(SyncMode mode)\n{\n    bool realtime_running = _state != RealtimeState::STOPPED;\n    _transport.set_sync_mode(mode, realtime_running);\n    if (realtime_running)\n    {\n        auto e = RtEvent::make_sync_mode_event(0, mode);\n        _send_control_event(e);\n    }\n}\n\nEngineReturnStatus AudioEngine::send_rt_event_to_processor(const RtEvent& event)\n{\n    auto status = _main_in_queue.push(event);\n    return status? EngineReturnStatus::OK : EngineReturnStatus::QUEUE_FULL;\n}\n\nEngineReturnStatus AudioEngine::_send_control_event(RtEvent& event)\n{\n    // This queue will only handle engine control events, not processor events\n    assert(is_engine_control_event(event));\n    std::lock_guard<std::mutex> lock(_in_queue_lock);\n    if (_control_queue_in.push(event))\n    {\n        return EngineReturnStatus::OK;\n    }\n    return EngineReturnStatus::QUEUE_FULL;\n}\n\nstd::pair<EngineReturnStatus, ObjectId> AudioEngine::create_multibus_track(const std::string& name, int bus_count, std::optional<int> thread)\n{\n    if (bus_count > MAX_TRACK_BUSES)\n    {\n        ELKLOG_LOG_ERROR(\"Invalid number of buses for new track\");\n        return {EngineReturnStatus::INVALID_N_CHANNELS, ObjectId(0)};\n    }\n    auto track = std::make_shared<Track>(_host_control, bus_count, &_process_timer);\n    auto status = _register_new_track(name, track, thread);\n    if (status != EngineReturnStatus::OK)\n    {\n        return {status, ObjectId(0)};\n    }\n    return {EngineReturnStatus::OK, track->id()};\n}\n\nstd::pair<EngineReturnStatus, ObjectId> AudioEngine::create_track(const std::string& name, int channel_count, std::optional<int> thread)\n{\n    if ((channel_count < 0 || channel_count > MAX_TRACK_CHANNELS))\n    {\n        ELKLOG_LOG_ERROR(\"Invalid number of channels for new track\");\n        return {EngineReturnStatus::INVALID_N_CHANNELS, ObjectId(0)};\n    }\n    // Only mono and stereo track have a pan parameter\n    bool pan_control = channel_count <= 2;\n\n    auto track = std::make_shared<Track>(_host_control, channel_count, &_process_timer, pan_control);\n    auto status = _register_new_track(name, track, thread);\n    if (status != EngineReturnStatus::OK)\n    {\n        return {status, ObjectId(0)};\n    }\n    return {EngineReturnStatus::OK, track->id()};\n}\n\nstd::pair<EngineReturnStatus, ObjectId> AudioEngine::create_post_track(const std::string& name)\n{\n    return _create_master_track(name, TrackType::POST, _audio_outputs);\n}\n\nstd::pair<EngineReturnStatus, ObjectId> AudioEngine::create_pre_track(const std::string& name)\n{\n    return _create_master_track(name, TrackType::PRE, _audio_inputs);\n}\n\nEngineReturnStatus AudioEngine::delete_track(ObjectId track_id)\n{\n    auto track = _processors.mutable_track(track_id);\n    if (track == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Couldn't delete track {}, not found\", track_id);\n        return EngineReturnStatus::INVALID_TRACK;\n    }\n    if (_processors.processors_on_track(track->id()).empty() == false)\n    {\n        ELKLOG_LOG_ERROR(\"Couldn't delete track {}, track not empty\", track_id);\n        return EngineReturnStatus::ERROR;\n    }\n\n    // First remove any audio connections, if realtime, this is done with RtEvents\n    _remove_connections_from_track(track->id());\n\n    if (realtime())\n    {\n        auto remove_event = RtEvent::make_remove_track_event(track->id());\n        auto delete_event = RtEvent::make_remove_processor_event(track->id());\n        _send_control_event(remove_event);\n        _send_control_event(delete_event);\n        bool removed = _event_receiver.wait_for_response(remove_event.returnable_event()->event_id(), RT_EVENT_TIMEOUT);\n        bool deleted = _event_receiver.wait_for_response(delete_event.returnable_event()->event_id(), RT_EVENT_TIMEOUT);\n        if (!removed || !deleted)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to remove processor {} from processing part\", track->name());\n        }\n    }\n    else\n    {\n        _remove_track(track.get());\n        [[maybe_unused]] bool removed = _remove_processor_from_realtime_part(track->id());\n        ELKLOG_LOG_WARNING_IF(removed == false, \"Plugin track {} was not in the audio graph\", track_id)\n    }\n    track->set_enabled(false);\n    _processors.remove_track(track->id());\n    _deregister_processor(track.get());\n\n    auto event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::TRACK_DELETED,\n                                                               0,\n                                                               track->id(),\n                                                               IMMEDIATE_PROCESS);\n    _event_dispatcher->post_event(std::move(event));\n    return EngineReturnStatus::OK;\n}\n\nstd::pair<EngineReturnStatus, ObjectId> AudioEngine::create_processor(const PluginInfo& plugin_info, const std::string &processor_name)\n{\n    auto [processor_status, processor] = _plugin_registry.new_instance(plugin_info, _host_control, _sample_rate);\n\n    if (processor_status != ProcessorReturnCode::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to initialize processor {} with error {}\", processor_name, static_cast<int>(processor_status));\n        return {to_engine_status(processor_status), ObjectId(0)};\n    }\n    EngineReturnStatus status = _register_processor(processor, processor_name);\n    if (status != EngineReturnStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to register processor {}\", processor_name);\n        return {status, ObjectId(0)};\n    }\n\n    if (this->realtime())\n    {\n        // In realtime mode we need to handle this in the audio thread\n        auto insert_event = RtEvent::make_insert_processor_event(processor.get());\n        _send_control_event(insert_event);\n        bool inserted = _event_receiver.wait_for_response(insert_event.returnable_event()->event_id(), RT_EVENT_TIMEOUT);\n        if (!inserted)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to insert processor {} to processing part\", processor_name);\n            _deregister_processor(processor.get());\n            return {EngineReturnStatus::INVALID_PROCESSOR, ObjectId(0)};\n        }\n    }\n    else\n    {\n        // If the engine is not running in realtime mode we can add the processor directly\n        _insert_processor_in_realtime_part(processor.get());\n    }\n\n    auto event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_CREATED,\n                                                               processor->id(),\n                                                               0,\n                                                               IMMEDIATE_PROCESS);\n\n    _event_dispatcher->post_event(std::move(event));\n    return {EngineReturnStatus::OK, processor->id()};\n}\n\nEngineReturnStatus AudioEngine::add_plugin_to_track(ObjectId plugin_id,\n                                                    ObjectId track_id,\n                                                    std::optional<ObjectId> before_plugin_id)\n{\n    auto track = _processors.mutable_track(track_id);\n    if (track == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Track {} not found\", track_id);\n        return EngineReturnStatus::INVALID_TRACK;\n    }\n\n    auto plugin = _processors.mutable_processor(plugin_id);\n    if (plugin == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Plugin {} not found\", plugin_id);\n        return EngineReturnStatus::INVALID_PLUGIN;\n    }\n\n    if (plugin->active_rt_processing())\n    {\n        ELKLOG_LOG_ERROR(\"Plugin {} is already active on a track\", plugin_id);\n        return EngineReturnStatus::ERROR;\n    }\n\n    int in_channels = std::min(plugin->max_input_channels(), track->input_channels());\n    int out_channels = std::min(plugin->max_output_channels(), track->input_channels());\n    plugin->set_channels(in_channels, out_channels);\n    plugin->set_enabled(true);\n\n    if (this->realtime())\n    {\n        // In realtime mode we need to handle this in the audio thread\n        RtEvent add_event = RtEvent::make_add_processor_to_track_event(plugin_id, track_id, before_plugin_id);\n        _send_control_event(add_event);\n        bool added = _event_receiver.wait_for_response(add_event.returnable_event()->event_id(), RT_EVENT_TIMEOUT);\n        if (added == false)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to add processor {} to track {}\", plugin->name(), track->name());\n            return EngineReturnStatus::ERROR;\n        }\n    }\n    else\n    {\n        // If the engine is not running in realtime mode we can add the processor directly\n        _insert_processor_in_realtime_part(plugin.get());\n        bool added = track->add(plugin.get(), before_plugin_id);\n        if (added == false)\n        {\n            return EngineReturnStatus::ERROR;\n        }\n    }\n    // Add it to the engine's mirror of track processing chains\n    _processors.add_to_track(plugin, track->id(), before_plugin_id);\n\n    auto event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_ADDED_TO_TRACK,\n                                                               plugin_id,\n                                                               track_id,\n                                                               IMMEDIATE_PROCESS);\n    _event_dispatcher->post_event(std::move(event));\n    return EngineReturnStatus::OK;\n}\n\nEngineReturnStatus AudioEngine::remove_plugin_from_track(ObjectId plugin_id, ObjectId track_id)\n{\n    auto plugin = _processors.mutable_processor(plugin_id);\n    auto track = _processors.mutable_track(track_id);\n    if (plugin == nullptr)\n    {\n        return EngineReturnStatus::INVALID_PLUGIN;\n    }\n    if (track == nullptr)\n    {\n        return EngineReturnStatus::INVALID_TRACK;\n    }\n\n    if (realtime())\n    {\n        // Send events to handle this in the rt domain\n        auto remove_event = RtEvent::make_remove_processor_from_track_event(plugin_id, track_id);\n        _send_control_event(remove_event);\n        if (!_event_receiver.wait_for_response(remove_event.returnable_event()->event_id(), RT_EVENT_TIMEOUT))\n        {\n            ELKLOG_LOG_ERROR(\"Failed to remove/delete processor {} in rt from track {}\", plugin_id, track_id);\n            return EngineReturnStatus::ERROR;\n        }\n    }\n    else\n    {\n        if (!track->remove(plugin->id()))\n        {\n            ELKLOG_LOG_ERROR(\"Failed to remove processor {} from track_id {}\", plugin_id, track_id);\n            return EngineReturnStatus::ERROR;\n        }\n    }\n\n    plugin->set_enabled(false);\n\n    bool removed = _processors.remove_from_track(plugin_id, track_id);\n    if (removed)\n    {\n        auto event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_REMOVED_FROM_TRACK,\n                                                                   plugin_id,\n                                                                   track_id,\n                                                                   IMMEDIATE_PROCESS);\n        _event_dispatcher->post_event(std::move(event));\n        return EngineReturnStatus::OK;\n    }\n    return EngineReturnStatus::ERROR;\n}\n\nEngineReturnStatus AudioEngine::delete_plugin(ObjectId plugin_id)\n{\n    auto processor = _processors.mutable_processor(plugin_id);\n    if (processor == nullptr)\n    {\n        return EngineReturnStatus::INVALID_PLUGIN;\n    }\n    if (processor->active_rt_processing())\n    {\n        ELKLOG_LOG_ERROR(\"Cannot delete processor {}, active on track\", processor->name());\n        return EngineReturnStatus::ERROR;\n    }\n    if (realtime())\n    {\n        // Send events to handle this in the rt domain\n        auto delete_event = RtEvent::make_remove_processor_event(processor->id());\n        _send_control_event(delete_event);\n        [[maybe_unused]] bool delete_ok = _event_receiver.wait_for_response(delete_event.returnable_event()->event_id(), RT_EVENT_TIMEOUT);\n        ELKLOG_LOG_ERROR_IF(delete_ok == false, \"Failed to remove/delete processor {} from processing part\", plugin_id)\n    }\n    else\n    {\n        _remove_processor_from_realtime_part(processor->id());\n    }\n\n    _deregister_processor(processor.get());\n\n    auto event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_DELETED,\n                                                               processor->id(),\n                                                               0,\n                                                               IMMEDIATE_PROCESS);\n\n    _event_dispatcher->post_event(std::move(event));\n    return EngineReturnStatus::OK;\n}\n\nEngineReturnStatus AudioEngine::_register_new_track(const std::string& name, std::shared_ptr<Track> track, std::optional<int> thread)\n{\n    track->init(_sample_rate);\n    track->set_enabled(true);\n\n    auto status = _register_processor(track, name);\n    if (status != EngineReturnStatus::OK)\n    {\n        track.reset();\n        return status;\n    }\n\n    int max_threads = std::max(0, _audio_graph.threads());\n    /*if (max_threads == 1 && thread.value_or(0) != 0) // TODO - maybe redundant case\n    {\n        ELKLOG_LOG_INFO(\"Track was set to run on thread {}, but sushi is running in single core mode\", thread.value_or(0));\n        thread = std::nullopt;\n    }*/\n    if (thread.value_or(0) >= max_threads)\n    {\n        ELKLOG_LOG_INFO(\"Track was set to run on thread {}, but sushi is configured to use {} threads, reverting to round-robin allocation\", thread.value_or(0), max_threads);\n        thread = std::nullopt;\n    }\n\n    if (realtime())\n    {\n        auto insert_event = RtEvent::make_insert_processor_event(track.get());\n        auto add_event = RtEvent::make_add_track_event(track->id(), thread);\n        _send_control_event(insert_event);\n        _send_control_event(add_event);\n        bool inserted = _event_receiver.wait_for_response(insert_event.returnable_event()->event_id(), RT_EVENT_TIMEOUT);\n        bool added = _event_receiver.wait_for_response(add_event.returnable_event()->event_id(), RT_EVENT_TIMEOUT);\n        if (!inserted || !added)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to insert/add track {} to processing part\", name);\n            return EngineReturnStatus::INVALID_PROCESSOR;\n        }\n    }\n    else\n    {\n        if (!_add_track(track.get(), thread))\n        {\n\n            ELKLOG_LOG_ERROR_IF(track->type() == TrackType::REGULAR, \"Error adding track {}, max number of tracks reached\", track->name());\n            ELKLOG_LOG_ERROR_IF(track->type() == TrackType::PRE, \"Error adding track {}, Only one pre track allowed\", track->name());\n            ELKLOG_LOG_ERROR_IF(track->type() == TrackType::POST, \"Error adding track {}, Only one post track allowed\", track->name());\n            return EngineReturnStatus::ERROR;\n        }\n        if (!_insert_processor_in_realtime_part(track.get()))\n        {\n            ELKLOG_LOG_ERROR(\"Error adding track {}\", track->name());\n            return EngineReturnStatus::ERROR;\n        }\n    }\n    if (_processors.add_track(track))\n    {\n        ELKLOG_LOG_INFO(\"Track {} successfully added to engine\", name);\n\n        auto event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::TRACK_CREATED,\n                                                                   0,\n                                                                   track->id(),\n                                                                   IMMEDIATE_PROCESS);\n\n        _event_dispatcher->post_event(std::move(event));\n        return EngineReturnStatus::OK;\n    }\n    return EngineReturnStatus::ERROR;\n}\n\nstd::pair<EngineReturnStatus, ObjectId> AudioEngine::_create_master_track(const std::string& name, TrackType type, int channels)\n{\n    auto track = std::make_shared<Track>(_host_control, channels, &_process_timer, false, type);\n    auto status = _register_new_track(name, track, std::nullopt);\n    if (status != EngineReturnStatus::OK)\n    {\n        return {status, ObjectId(0)};\n    }\n    return {EngineReturnStatus::OK, track->id()};\n}\n\nEngineReturnStatus AudioEngine::_connect_audio_channel(int engine_channel,\n                                                       int track_channel,\n                                                       ObjectId track_id,\n                                                       Direction direction)\n{\n    auto track = _processors.mutable_track(track_id);\n    if (track == nullptr)\n    {\n        return EngineReturnStatus::INVALID_TRACK;\n    }\n\n    if (direction == Direction::INPUT)\n    {\n        if (engine_channel >= _audio_inputs || track_channel >= track->input_channels())\n        {\n            return EngineReturnStatus::INVALID_CHANNEL;\n        }\n    }\n    else\n    {\n        if (engine_channel >= _audio_outputs || track_channel >= track->output_channels())\n        {\n            if (track_channel == 1 &&\n                track->max_output_channels() == 2 &&\n                track->output_channels() <= 1)\n            {\n                // Corner case when connecting a mono track to a stereo output bus, this is allowed\n                track->set_channels(1, 2);\n            }\n            else\n            {\n                return EngineReturnStatus::INVALID_CHANNEL;\n            }\n        }\n    }\n\n    auto& storage = direction == Direction::INPUT ? _audio_in_connections : _audio_out_connections;\n    bool realtime = this->realtime();\n\n    AudioConnection con = {.engine_channel = engine_channel,\n                           .track_channel = track_channel,\n                           .track = track->id()};\n    bool added = storage.add(con, !realtime);\n\n    if (added && realtime)\n    {\n        auto event = direction == Direction::INPUT ? RtEvent::make_add_audio_input_connection_event(con) :\n                                                     RtEvent::make_add_audio_output_connection_event(con);\n        _send_control_event(event);\n        added = _event_receiver.wait_for_response(event.returnable_event()->event_id(), RT_EVENT_TIMEOUT);\n        if (added == false)\n        {\n            storage.remove(con, false);\n            ELKLOG_LOG_ERROR(\"Failed to insert audio connection in realtime thread\");\n        }\n    }\n    else if (added == false)\n    {\n        ELKLOG_LOG_ERROR(\"Max number of {} audio connections reached\", direction == Direction::INPUT ? \"input\" : \"output\");\n        return EngineReturnStatus::ERROR;\n    }\n\n    ELKLOG_LOG_INFO(\"Connected engine {} {} to channel {} of track \\\"{}\\\"\",\n                        direction == Direction::INPUT ? \"input\" : \"output\", engine_channel, track_channel, track_id);\n    return EngineReturnStatus::OK;\n}\n\nEngineReturnStatus AudioEngine::_disconnect_audio_channel(int engine_channel,\n                                                          int track_channel,\n                                                          ObjectId track_id,\n                                                          Direction direction)\n{\n    auto track = _processors.track(track_id);\n    if (track == nullptr)\n    {\n        return EngineReturnStatus::INVALID_TRACK;\n    }\n\n    auto& storage = direction == Direction::INPUT ? _audio_in_connections : _audio_out_connections;\n    bool realtime = this->realtime();\n\n    AudioConnection con = {.engine_channel = engine_channel, .track_channel = track_channel, .track = track->id()};\n    bool removed = storage.remove(con, !realtime);\n\n    if (removed && realtime)\n    {\n        auto event = direction == Direction::INPUT ? RtEvent::make_remove_audio_input_connection_event(con) :\n                                                     RtEvent::make_remove_audio_output_connection_event(con);\n        _send_control_event(event);\n        removed = _event_receiver.wait_for_response(event.returnable_event()->event_id(), RT_EVENT_TIMEOUT);\n        ELKLOG_LOG_ERROR_IF(removed == false, \"Failed to remove audio connection in realtime thread\")\n    }\n    else if (removed == false)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to remove {} audio connection\", direction == Direction::INPUT ? \"input\" : \"output\");\n        return EngineReturnStatus::ERROR;\n    }\n\n    ELKLOG_LOG_INFO(\"Removed {} audio connection from channel {} of track \\\"{}\\\" and engine channel {}\",\n                         direction == Direction::INPUT ? \"input\" : \"output\", track_channel, track->name(), engine_channel);\n    return EngineReturnStatus::OK;\n}\n\nvoid AudioEngine::_process_internal_rt_events()\n{\n    RtEvent event;\n    while (_control_queue_in.pop(event))\n    {\n        switch (event.type())\n        {\n            case RtEventType::TEMPO:\n            case RtEventType::TIME_SIGNATURE:\n            case RtEventType::PLAYING_MODE:\n            case RtEventType::SYNC_MODE:\n            {\n                _transport.process_event(event);\n                break;\n            }\n            case RtEventType::INSERT_PROCESSOR:\n            {\n                auto typed_event = event.processor_operation_event();\n                bool inserted = _insert_processor_in_realtime_part(typed_event->instance());\n                typed_event->set_handled(inserted);\n                break;\n            }\n            case RtEventType::REMOVE_PROCESSOR:\n            {\n                auto typed_event = event.processor_reorder_event();\n                bool removed = _remove_processor_from_realtime_part(typed_event->processor());\n                typed_event->set_handled(removed);\n                break;\n            }\n            case RtEventType::ADD_PROCESSOR_TO_TRACK:\n            {\n                auto typed_event = event.processor_reorder_event();\n                auto track = static_cast<Track*>(_realtime_processors[typed_event->track()]);\n                auto processor = static_cast<Processor*>(_realtime_processors[typed_event->processor()]);\n                bool added = false;\n                if (track && processor)\n                {\n                    added = track->add(processor, typed_event->before_processor());\n                }\n                typed_event->set_handled(added);\n                break;\n            }\n            case RtEventType::REMOVE_PROCESSOR_FROM_TRACK:\n            {\n                auto typed_event = event.processor_reorder_event();\n                auto track = static_cast<Track*>(_realtime_processors[typed_event->track()]);\n                bool removed = false;\n                if (track)\n                {\n                    removed = track->remove(typed_event->processor());\n                }\n                typed_event->set_handled(removed);\n                break;\n            }\n            case RtEventType::ADD_TRACK:\n            {\n                auto typed_event = event.track_event();\n                auto track = static_cast<Track*>(_realtime_processors[typed_event->track()]);\n                auto thread = typed_event->thread();\n                typed_event->set_handled(_add_track(track, thread));\n                break;\n            }\n            case RtEventType::REMOVE_TRACK:\n            {\n                auto typed_event = event.track_event();\n                auto track = static_cast<Track*>(_realtime_processors[typed_event->track()]);\n                typed_event->set_handled(_remove_track(track));\n                break;\n            }\n            case RtEventType::ADD_AUDIO_CONNECTION:\n            {\n                auto typed_event = event.audio_connection_event();\n                assert(_realtime_processors[typed_event->connection().track]);\n                auto& storage = typed_event->input_connection() ? _audio_in_connections : _audio_out_connections;\n                typed_event->set_handled(storage.add_rt(typed_event->connection()));\n                break;\n            }\n            case RtEventType::REMOVE_AUDIO_CONNECTION:\n            {\n                auto typed_event = event.audio_connection_event();\n                auto& storage = typed_event->input_connection() ? _audio_in_connections : _audio_out_connections;\n                typed_event->set_handled(storage.remove_rt(typed_event->connection()));\n                break;\n            }\n\n            default:\n                break;\n        }\n        _control_queue_out.push(event); // Send event back to non-rt domain\n    }\n}\n\nvoid AudioEngine::_send_rt_events_to_processors()\n{\n    RtEvent event;\n    while (_main_in_queue.pop(event))\n    {\n        _send_rt_event(event);\n    }\n}\n\nvoid AudioEngine::_send_rt_event(const RtEvent& event)\n{\n    if (event.processor_id() < _realtime_processors.size() &&\n        _realtime_processors[event.processor_id()] != nullptr)\n    {\n        _realtime_processors[event.processor_id()]->process_event(event);\n    }\n}\n\nvoid AudioEngine::_retrieve_events_from_tracks(ControlBuffer& buffer)\n{\n    for (auto& output : _audio_graph.event_outputs())\n    {\n        _retrieve_events_from_output_pipe(output, buffer);\n    }\n    _retrieve_events_from_output_pipe(_prepost_event_outputs, buffer);\n}\n\nvoid AudioEngine::_retrieve_events_from_output_pipe(RtEventFifo<>& pipe, ControlBuffer& buffer)\n{\n    while (pipe.empty() == false)\n    {\n        const RtEvent& event = pipe.pop();\n        switch (event.type())\n        {\n            case RtEventType::CV_EVENT:\n            {\n                auto typed_event = event.cv_event();\n                buffer.cv_values[typed_event->cv_id()] = typed_event->value();\n                break;\n            }\n\n            case RtEventType::GATE_EVENT:\n            {\n                auto typed_event = event.gate_event();\n                _outgoing_gate_values[typed_event->gate_no()] = typed_event->value();\n                break;\n            }\n\n            default:\n                _main_out_queue.push(event);\n        }\n    }\n    buffer.gate_values = _outgoing_gate_values;\n}\n\nvoid AudioEngine::_copy_audio_to_tracks(ChunkSampleBuffer* input)\n{\n    for (const auto& c : _audio_in_connections.connections_rt())\n    {\n        auto engine_in = ChunkSampleBuffer::create_non_owning_buffer(*input, c.engine_channel, 1);\n        auto track_in = static_cast<Track*>(_realtime_processors[c.track])->input_channel(c.track_channel);\n        track_in = engine_in;\n    }\n}\n\nvoid AudioEngine::_copy_audio_from_tracks(ChunkSampleBuffer* output)\n{\n    output->clear();\n    for (const auto& c : _audio_out_connections.connections_rt())\n    {\n        auto track_out = static_cast<Track*>(_realtime_processors[c.track])->output_channel(c.track_channel);\n        auto engine_out = ChunkSampleBuffer::create_non_owning_buffer(*output, c.engine_channel, 1);\n        engine_out.add(track_out);\n    }\n}\n\nvoid AudioEngine::update_timings()\n{\n    if (_process_timer.enabled())\n    {\n        std::vector<performance::ProcessTimings> thread_timings;\n        auto engine_timings = _process_timer.timings_for_node(ENGINE_TIMING_ID);\n\n        if (engine_timings.has_value())\n        {\n            int thread_id = ENGINE_TIMING_ID; // Thread ids are counted backwards from the main cpu id\n            std::optional<performance::ProcessTimings> timings;\n            while ((timings = _process_timer.timings_for_node(--thread_id)).has_value())\n            {\n                thread_timings.push_back(timings.value());\n            }\n            _event_dispatcher->post_event(std::make_unique<EngineTimingNotificationEvent>(*engine_timings, thread_timings, IMMEDIATE_PROCESS));\n        }\n\n        _log_timing_print_counter += 1;\n        if (_log_timing_print_counter > TIMING_LOG_PRINT_INTERVAL)\n        {\n            for (const auto& processor : _processors.all_processors())\n            {\n                auto id = processor->id();\n                auto timings = _process_timer.timings_for_node(id);\n                if (timings.has_value())\n                {\n                    ELKLOG_LOG_INFO(\"Processor: {} ({}), avg: {}%, min: {}%, max: {}%\", id, processor->name(),\n                                    timings->avg_case * 100.0f, timings->min_case * 100.0f, timings->max_case * 100.0f);\n                }\n            }\n\n            if (engine_timings.has_value())\n            {\n                ELKLOG_LOG_INFO(\"Engine total: avg: {}%, min: {}%, max: {}%\",\n                                engine_timings->avg_case * 100.0f, engine_timings->min_case * 100.0f, engine_timings->max_case * 100.0f);\n            }\n            [[maybe_unused]] int thread_id = 0;\n            for ([[maybe_unused]] const auto& timings : thread_timings)\n            {\n                ELKLOG_LOG_INFO(\"Audio thread: {}, avg: {}%, min: {}%, max: {}%\", thread_id++,\n                                timings.avg_case * 100.0f, timings.min_case * 100.0f, timings.max_case * 100.0f);\n            }\n            _log_timing_print_counter = 0;\n        }\n    }\n}\n\nvoid AudioEngine::notify_interrupted_audio(Time interrupt_time)\n{\n    if (interrupt_time > RT_EVENT_TIMEOUT / 2 )\n    {\n        /* If audio was paused for long enough, pending RtEvents (add/remove processor, etc)\n         * May have timed out and should not be processed. Also assume that midi notes and\n         * parameter changes should not take effect */\n        ELKLOG_LOG_INFO(\"Rt thread timed out for too long, clearing queues\");\n        RtEvent event;\n        while (_control_queue_in.pop(event))\n        {}\n        while (_main_in_queue.pop(event))\n        {}\n    }\n}\n\nvoid print_single_timings_for_node(std::fstream& f, performance::PerformanceTimer& timer, int id)\n{\n    auto timings = timer.timings_for_node(id);\n    if (timings.has_value())\n    {\n        f << std::setw(16) << timings.value().avg_case * 100.0\n          << std::setw(16) << timings.value().min_case * 100.0\n          << std::setw(16) << timings.value().max_case * 100.0 << \"\\n\";\n    }\n}\n\nbool AudioEngine::_add_track(Track* track, std::optional<int> thread)\n{\n    bool added = false;\n    switch (track->type())\n    {\n        case TrackType::REGULAR:\n            if (thread.has_value())\n            {\n                added = _audio_graph.add_to_thread(track, *thread);\n            }\n            else\n            {\n                added = _audio_graph.add(track);\n            }\n            break;\n\n        case TrackType::POST:\n            if (_post_track == nullptr)\n            {\n                _post_track = track;\n                _post_track->set_event_output(&_prepost_event_outputs);\n                added = true;\n            }\n            break;\n\n        case TrackType::PRE:\n            if (_pre_track == nullptr)\n            {\n                _pre_track = track;\n                _pre_track->set_event_output(&_prepost_event_outputs);\n                added = true;\n            }\n    }\n    return added;\n}\n\nbool AudioEngine::_remove_track(Track* track)\n{\n    bool removed = false;\n    switch (track->type())\n    {\n        case TrackType::REGULAR:\n            removed = _audio_graph.remove(track);\n            break;\n\n        case TrackType::POST:\n            if (_post_track)\n            {\n                _post_track = nullptr;\n                removed = true;\n            }\n            break;\n\n        case TrackType::PRE:\n            if (_pre_track)\n            {\n                _pre_track = nullptr;\n                removed = true;\n            }\n    }\n    return removed;\n}\n\nvoid AudioEngine::print_timings_to_file(const std::string& filename)\n{\n    std::fstream file;\n    file.open(filename, std::ios_base::out);\n\n    if (!file.is_open())\n    {\n        ELKLOG_LOG_WARNING(\"Couldn't write timings to file\");\n        return;\n    }\n    file.setf(std::ios::left);\n    file << \"Performance timings for all processors in percentages of audio buffer (100% = \"<< 1000000.0 / _sample_rate * AUDIO_CHUNK_SIZE\n         << \"us)\\n\\n\" << std::setw(24) << \"\" << std::setw(16) << \"average(%)\" << std::setw(16) << \"minimum(%)\"\n         << std::setw(16) << \"maximum(%)\" << std::endl;\n\n    for (const auto& track : _processors.all_tracks())\n    {\n        file << std::setw(0) << \"Track: \" << track->name() << \"\\n\";\n        for (auto& p : _processors.processors_on_track(track->id()))\n        {\n            file << std::setw(8) << \"\" << std::setw(16) << p->name();\n            print_single_timings_for_node(file, _process_timer, p->id());\n        }\n        file << std::setw(8) << \"\" << std::setw(16) << \"Track total\";\n        print_single_timings_for_node(file, _process_timer, track->id());\n        file << \"\\n\";\n    }\n\n    file << std::setw(24) << \"Engine total\";\n    print_single_timings_for_node(file, _process_timer, ENGINE_TIMING_ID);\n    file.close();\n\n    for (auto id : _process_timer.enabled_detailed_timings())\n    {\n        auto processor = _processors.processor(id);\n        if (processor)\n        {\n            std::string filename = \"detailed_timings_\" + std::to_string(id) + \"_\" + processor->name() + \"_ns.csv\";\n            file.open(filename, std::ios_base::out);\n            for (const auto i : _process_timer.detailed_timings_for_node(id))\n            {\n                file << i.count() << \",\";\n            }\n            file.close();\n        }\n    }\n}\n\nvoid AudioEngine::_route_cv_gate_ins(ControlBuffer& buffer)\n{\n    for (const auto& r : _cv_in_connections)\n    {\n        float value = buffer.cv_values[r.cv_id];\n        auto ev = RtEvent::make_parameter_change_event(r.processor_id, 0, r.parameter_id, value);\n        _send_rt_event(ev);\n    }\n\n    // Get gate state changes by xor:ing with previous states\n    auto gate_diffs = _prev_gate_values ^ buffer.gate_values;\n    if (gate_diffs.any())\n    {\n        for (const auto& r : _gate_in_connections)\n        {\n            if (gate_diffs[r.gate_id])\n            {\n                auto gate_high = buffer.gate_values[r.gate_id];\n                if (gate_high)\n                {\n                    auto ev = RtEvent::make_note_on_event(r.processor_id, 0, r.channel, r.note_no, 1.0f);\n                    _send_rt_event(ev);\n                }\n                else\n                {\n                    auto ev = RtEvent::make_note_off_event(r.processor_id, 0, r.channel, r.note_no, 1.0f);\n                    _send_rt_event(ev);\n                }\n            }\n        }\n    }\n    _prev_gate_values = buffer.gate_values;\n}\n\nRealtimeState update_state(RealtimeState current_state)\n{\n    if (current_state == RealtimeState::STARTING)\n    {\n        return RealtimeState::RUNNING;\n    }\n\n    if (current_state == RealtimeState::STOPPING)\n    {\n        return RealtimeState::STOPPED;\n    }\n\n    return current_state;\n}\n\n} // end namespace sushi::internal::engine\n"
  },
  {
    "path": "src/engine/audio_engine.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Real time audio processing engine\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_ENGINE_H\n#define SUSHI_ENGINE_H\n\n#include <vector>\n#include <utility>\n#include <mutex>\n\n#include \"twine/twine.h\"\n\n#include \"sushi/sushi_time.h\"\n#include \"sushi/types.h\"\n#include \"sushi/sample_buffer.h\"\n\n#include \"dsp_library/master_limiter.h\"\n\n#include \"engine/audio_graph.h\"\n#include \"engine/base_engine.h\"\n#include \"engine/connection_storage.h\"\n#include \"engine/controller/controller.h\"\n#include \"engine/event_dispatcher.h\"\n#include \"engine/host_control.h\"\n#include \"engine/plugin_library.h\"\n#include \"engine/processor_container.h\"\n#include \"engine/receiver.h\"\n#include \"engine/track.h\"\n#include \"engine/transport.h\"\n\n#include \"library/internal_plugin.h\"\n#include \"library/midi_decoder.h\"\n#include \"library/performance_timer.h\"\n#include \"library/plugin_registry.h\"\n#include \"library/rt_event_fifo.h\"\n\nnamespace sushi::internal::engine {\n\nclass ClipDetector\n{\npublic:\n    explicit ClipDetector(float sample_rate)\n    {\n        this->set_sample_rate(sample_rate);\n    }\n\n    void set_sample_rate(float sample_rate);\n\n    void set_channels(int inputs, int outputs);\n\n    /**\n     * @brief Find clipped samples in a buffer and send notifications\n     * @param buffer The audio buffer to process\n     * @param queue Endpoint for clipping notifications\n     * @param audio_input Set to true if the audio buffer comes directly from the an audio inout (i.e. before any processing)\n     */\n    void detect_clipped_samples(const ChunkSampleBuffer& buffer, RtSafeRtEventFifo& queue, bool audio_input);\n\nprivate:\n    unsigned int _interval{0};\n    std::vector<unsigned int> _input_clip_count;\n    std::vector<unsigned int> _output_clip_count;\n};\n\nconstexpr int MAX_RT_PROCESSOR_ID = 100000;\n\nclass AudioEngineAccessor;\n\nclass AudioEngine : public BaseEngine\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(AudioEngine)\n\n    /**\n     * @brief Construct a new AudioEngine\n     * @param sample_rate The sample to use in Hz\n     * @param rt_threads The maximum number of cpu cores to use for audio processing. Default\n     *                   is 1 and means that audio processing is done only in the rt callback\n     *                   of the audio frontend.\n     *                   With values > 1 tracks will be processed in parallel threads.\n     * @param device_name The audio device name - only used on Apple for fetching the audio thread workgroup, and will be unused on other platforms.\n     * @param debug_mode_sw Enable xenomai specific thread debugging for all audio threads in\n     *                      multicore mode.\n     * @param event_dispatcher A pointer to a BaseEventDispatcher instance, which AudioEngine takes over ownership of.\n     *                         If nullptr, a normal EventDispatcher is created and used.\n     */\n    explicit AudioEngine(float sample_rate,\n                         int rt_threads = 1,\n                         std::optional<std::string> device_name = std::nullopt,\n                         bool debug_mode_sw = false,\n                         dispatcher::BaseEventDispatcher* event_dispatcher = nullptr);\n\n     ~AudioEngine() override;\n\n    /**\n     * @brief Configure the Engine with a new samplerate.\n     * @param sample_rate The new sample rate in Hz\n     */\n    void set_sample_rate(float sample_rate) override;\n\n    /**\n     * @brief Set the number of audio channels, set by the audio frontend before\n     *        starting processing\n     * @param inputs The number of input audio channels to use\n     * @param outputs The number of output audio channels to use\n     */\n    void set_audio_channels(int inputs, int outputs) override;\n\n    /**\n     * @brief Set the number of control voltage inputs, set by the audio frontend before\n     *        starting processing\n     * @param channels The number of cv input channels to use\n     * @return EngineReturnStatus::OK if successful, error status otherwise\n     */\n    EngineReturnStatus set_cv_input_channels(int channels) override;\n\n    /**\n     * @brief Set the number of control voltage outputs, set by the audio frontend before\n     *        starting processing\n     * @param channels The number of cv input channels to use\n     * @return EngineReturnStatus::OK if successful, error status otherwise\n     */\n    EngineReturnStatus set_cv_output_channels(int channels) override;\n\n    /**\n     * @brief Connect an engine input channel to an input channel of a given track.\n     *        Not safe to call while the engine is running.\n     * @param input_channel Index of the engine input channel to connect.\n     * @param track_channel Index of the input channel of the track to connect to.\n     * @param track_id The id of the track to connect to.\n     * @return EngineReturnStatus::OK if successful, error status otherwise\n     */\n    EngineReturnStatus connect_audio_input_channel(int input_channel,\n                                                   int track_channel,\n                                                   ObjectId track_id) override;\n\n    /**\n     * @brief Connect an output channel of a track to an engine output channel.\n     *        Not safe to call while the engine is running.\n     * @param output_channel Index of the engine output channel to connect to.\n     * @param track_channel Index of the output channel of the track to connect from.\n     * @param track_id The id of the track to connect from.\n     * @return EngineReturnStatus::OK if successful, error status otherwise\n     */\n    EngineReturnStatus connect_audio_output_channel(int output_channel,\n                                                    int track_channel,\n                                                    ObjectId track_id) override;\n\n    EngineReturnStatus disconnect_audio_input_channel(int engine_channel,\n                                                      int track_channel,\n                                                      ObjectId track_id) override;\n\n    EngineReturnStatus disconnect_audio_output_channel(int engine_channel,\n                                                      int track_channel,\n                                                      ObjectId track_id) override;\n\n    std::vector<AudioConnection> audio_input_connections() override;\n\n    std::vector<AudioConnection> audio_output_connections() override;\n\n    /**\n     * @brief Connect a stereo pair (bus) from an engine input bus to an input bus of\n     *        given track. Not safe to use while the engine in running.\n     * @param input_bus The engine input bus to use.\n     *        bus 0 refers to channels 0-1, 1 to channels 2-3, etc\n     * @param track_bus The input bus of the track to connect to.\n     * @param track_id The id of the track to connect to.\n     * @return EngineReturnStatus::OK if successful, error status otherwise\n     */\n    EngineReturnStatus connect_audio_input_bus(int input_bus,\n                                               int track_bus,\n                                               ObjectId track_id) override;\n\n    /**\n     * @brief Connect an output bus of a track to an output bus (stereo pair)\n     *        Not safe to use while the engine in running.\n     * @param output_bus The engine outpus bus to use.\n     *        bus 0 refers to channels 0-1, 1 to channels 2-3, etc\n     * @param track_bus The output bus of the track to connect from.\n     * @param track_id The id of the track to connect from.\n     * @return EngineReturnStatus::OK if successful, error status otherwise\n     */\n    EngineReturnStatus connect_audio_output_bus(int output_bus,\n                                                int track_bus,\n                                                ObjectId track_id) override;\n\n    /**\n     * @brief Connect a control voltage input to control a parameter on a processor\n     * @param processor_name The unique name of the processor.\n     * @param parameter_name The name of the parameter to connect to\n     * @param cv_input_id The Cv input port id to use\n     * @return EngineReturnStatud::OK if successful, error status otherwise\n     */\n    EngineReturnStatus connect_cv_to_parameter(const std::string& processor_name,\n                                               const std::string& parameter_name,\n                                               int cv_input_id) override;\n\n    /**\n     * @brief Connect a parameter on a processor to a control voltage output\n     * @param processor_name The unique name of the processor.\n     * @param parameter_name The name of the parameter to connect from\n     * @param cv_output_id The Cv output port id to use\n     * @return EngineReturnStatus::OK if successful, error status otherwise\n     */\n    EngineReturnStatus connect_cv_from_parameter(const std::string& processor_name,\n                                                 const std::string& parameter_name,\n                                                 int cv_output_id) override;\n\n    /**\n     * @brief Connect a gate input to a processor. Gate changes will be sent as note\n     *        on or note off messages to the processor on the selected channel and\n     *        with the selected note number.\n     * @param processor_name The unique name of the processor.\n     * @param gate_input_id The gate input port to use.\n     * @param note_no The note number to identify the gate id (0-127).\n     * @param channel The channel to use (0-15).\n     * @return EngineReturnStatus::OK if successful, error status otherwise\n     */\n    EngineReturnStatus connect_gate_to_processor(const std::string& processor_name,\n                                                 int gate_input_id,\n                                                 int note_no,\n                                                 int channel) override;\n\n    /**\n     * @brief Connect a processor to a gate output. Note on or note off messages sent\n     *        from the processor on the selected channel and note number will be converted\n     *        to gate events on the gate output.\n     * @param processor_name The unique name of the processor.\n     * @param gate_output_id The gate output port to use.\n     * @param note_no The note number to identify the gate id (0-127)\n     * @param channel The channel to use (0-15).\n     * @return EngineReturnStatus::OK if successful, error status otherwise\n     */\n    EngineReturnStatus connect_gate_from_processor(const std::string& processor_name,\n                                                   int gate_output_id,\n                                                   int note_no,\n                                                   int channel) override;\n\n    /**\n     * @brief Use a selected gate input as sync input.\n     * @param gate_input_id The gate input port to use as sync input.\n     * @param ppq_ticks Number of ticks per quarternote.\n     * @return EngineReturnStatus::OK if successful, error status otherwise\n     */\n    EngineReturnStatus connect_gate_to_sync(int gate_input_id,\n                                            int ppq_ticks) override;\n\n    /**\n     * @brief Send sync pulses on a gate output.\n     * @param gate_output_id The gate output to use.\n     * @param ppq_ticks Number of pulses per quarternote.\n     * @return EngineReturnStatus::OK if successful, error status otherwise\n     */\n    EngineReturnStatus connect_sync_to_gate(int gate_output_id,\n                                            int ppq_ticks) override;\n\n    /**\n     * @brief Returns whether the engine is running in a realtime mode or not\n     * @return true if the engine is currently processing in realtime mode, false otherwise\n     */\n    bool realtime() override;\n\n    /**\n     * @brief Set the engine to operate in realtime mode. In this mode process_chunk and\n     *        _send_rt_event is assumed to be called from a realtime thread.\n     *        All other function calls are assumed to be made from non-realtime threads\n     *\n     * @param enabled true to enable realtime mode, false to disable\n     */\n    void enable_realtime(bool enabled) override;\n\n    /**\n     * @brief Process one chunk of audio from in_buffer and write the result to out_buffer\n     * @param in_buffer input audio buffer\n     * @param out_buffer output audio buffer\n     * @param in_controls input control voltage and gate data\n     * @param out_controls output control voltage and gate data\n     * @param timestamp Current time in microseconds\n     * @param sample_count Current number of samples processed\n     */\n    void process_chunk(SampleBuffer<AUDIO_CHUNK_SIZE>* in_buffer,\n                       SampleBuffer<AUDIO_CHUNK_SIZE>* out_buffer,\n                       ControlBuffer* in_controls,\n                       ControlBuffer* out_controls,\n                       Time timestamp,\n                       int64_t sample_count) override;\n\n    /**\n     * @brief Inform the engine of the current system latency\n     * @param latency The output latency of the audio system\n     */\n    void set_output_latency(Time latency) override\n    {\n        _transport.set_latency(latency);\n    }\n\n    /**\n     * @brief Set the tempo of the engine. Intended to be called from a non-realtime thread.\n     * @param tempo The new tempo in beats (quarter notes) per minute\n     */\n    void set_tempo(float tempo) override;\n\n    /**\n     * @brief Set the time signature of the engine. Intended to be called from a non-realtime thread.\n     * @param signature A TimeSignature object describing the new time signature to use\n     */\n    void set_time_signature(TimeSignature signature) override;\n\n    /**\n     * @brief Set the current transport mode, i.e stopped, playing, recording. This will be\n     *        passed on to processors. Note stopped here means that audio is still running\n     *        but sequencers and similar should be in a stopped state.\n     *        Currently only STOPPED and PLAYING are implemented and default is PLAYING\n     * @param mode A TransportMode mode with the current state\n     */\n    void set_transport_mode(PlayingMode mode) override;\n\n    /**\n     * @brief Set the current mode of synchronising the engine tempo and beats. Default is\n     *        INTERNAL.\n     * @param mode A SyncMode with the current mode of synchronisation\n     */\n    void set_tempo_sync_mode(SyncMode mode) override;\n\n    /**\n     * @brief Set an absolute path to be the base for plugin paths\n     *\n     * @param path Absolute path of the base plugin folder\n     */\n    void set_base_plugin_path(const std::string& path) override\n    {\n        _plugin_library.set_base_plugin_path(path);\n    }\n\n    /**\n     * @brief Send an RtEvent to a processor directly to the realtime thread. Should normally only be used\n     *        from an rt thread or in a context where the engine is not running in realtime mode.\n     * @param event The event to process\n     * @return EngineReturnStatus::OK if the event was properly processed, error code otherwise\n     */\n    EngineReturnStatus send_rt_event_to_processor(const RtEvent& event) override;\n\n    /**\n     * @brief Create an empty track\n     * @param name The unique name of the track to be created.\n     * @param channel_count The number of channels in the track.\n     * @param thread The audio processing thread to allocate the track to\n     * @return the unique id of the track created, only valid if status is EngineReturnStatus::OK\n     */\n    std::pair<EngineReturnStatus, ObjectId> create_track(const std::string& name,\n                                                         int channel_count,\n                                                         std::optional<int> thread) override;\n\n    /**\n     * @brief Create an empty track\n     * @param name The unique name of the track to be created.\n     * @param bus_count The number of stereo pairs in the track.\n     * @return EngineInitStatus::OK in case of success, different error code otherwise.\n     */\n    std::pair<EngineReturnStatus, ObjectId> create_multibus_track(const std::string& name, int bus_count, std::optional<int> thread) override;\n\n    std::pair<EngineReturnStatus, ObjectId> create_post_track(const std::string& name) override;\n\n    std::pair<EngineReturnStatus, ObjectId> create_pre_track(const std::string& name) override;\n\n    /**\n     * @brief Delete a track, currently assumes that the track is empty before calling\n     * @param track_id The unique name of the track to delete\n     * @return EngineReturnStatus::OK in case of success, different error code otherwise.\n     */\n    EngineReturnStatus delete_track(ObjectId track_id) override;\n\n    /**\n     * @brief Create a processor instance, either from internal plugins or loaded from file.\n     *        The created plugin can then be added to tracks.\n     * @param plugin_info\n     * @param processor_name\n     * @return\n     */\n    std::pair<EngineReturnStatus, ObjectId> create_processor(const PluginInfo& plugin_info,\n                                                             const std::string& processor_name) override;\n\n    /**\n     * @brief Add a plugin to a track. The plugin must not currently be active on any track.\n     * @param track_id The id of the track to add the plugin to.\n     * @param plugin_id The id of the plugin to add.\n     * @param before_plugin_id If this parameter is passed with a value, the plugin will be\n     *        inserted after this plugin. If this parameter is empty, the plugin will be\n     *        placed at the back of the track's processing chain.\n     * @return EngineReturnStatus::OK in case of success, different error code otherwise.\n     */\n    EngineReturnStatus add_plugin_to_track(ObjectId plugin_id,\n                                           ObjectId track_id,\n                                           std::optional<ObjectId> before_plugin_id = std::nullopt) override;\n\n    /**\n     * @brief Remove a given plugin from a track and delete it\n     * @param track_id The id of the track that contains the plugin\n     * @param plugin_id The id of the plugin\n     * @return EngineReturnStatus::OK in case of success, different error code otherwise\n     */\n    EngineReturnStatus remove_plugin_from_track(ObjectId plugin_id, ObjectId track_id) override;\n\n    /**\n     * @brief Delete and unload a plugin instance from Sushi. The plugin must\n     *        not currently be active on any track.\n     * @param plugin_id The id of the plugin to delete.\n     * @return EngineReturnStatus::OK in case of success, different error code otherwise.\n     */\n    EngineReturnStatus delete_plugin(ObjectId plugin_id) override;\n\n    /**\n     * @brief Enable audio clip detection on engine inputs\n     * @param enabled Enable if true, disable if false\n     */\n    void enable_input_clip_detection(bool enabled) override\n    {\n        _input_clip_detection_enabled = enabled;\n    }\n\n    /**\n    * @brief Return the state of clip detection on audio inputs\n    * @param true if clip detection is enabled, false if disabled.\n    */\n    bool input_clip_detection() const override\n    {\n        return _input_clip_detection_enabled;\n    }\n\n    /**\n     * @brief Enable audio clip detection on engine outputs\n     * @param enabled Enable if true, disable if false\n     */\n    void enable_output_clip_detection(bool enabled) override\n    {\n        _output_clip_detection_enabled = enabled;\n    }\n\n    /**\n    * @brief Return the state of clip detection on outputs\n    * @param true if clip detection is enabled, false if disabled.\n    */\n    bool output_clip_detection() const override\n    {\n        return _output_clip_detection_enabled;\n    }\n\n    /**\n     * @brief Enable master limiter on outputs\n     * @param enabled Enabled if true, disable if false\n     */\n    void enable_master_limiter(bool enabled) override\n    {\n        _master_limiter_enabled = enabled;\n    }\n\n    /**\n     * @brief Return the state of master limiter on outputs\n     * @param true if master limiter is enabled, false if disabled.\n     */\n    bool master_limiter() const override\n    {\n        return _master_limiter_enabled;\n    }\n\n    dispatcher::BaseEventDispatcher* event_dispatcher() override\n    {\n        return _event_dispatcher.get();\n    }\n\n    engine::Transport* transport() override\n    {\n        return &_transport;\n    }\n\n    performance::BasePerformanceTimer* performance_timer() override\n    {\n        return &_process_timer;\n    }\n\n    const BaseProcessorContainer* processor_container() override\n    {\n        return &_processors;\n    }\n\n    /**\n     * @brief Print the current processor timings (if enabled) in the log\n     */\n    void update_timings() override;\n\n    /**\n     * @brief Notify sushi that audio processing was interrupted. Possibly due to to\n     *        audio over/underruns, audio was paused or similar issues\n     * @param duration An estimation of how long the interrupt lasted\n     */\n    void notify_interrupted_audio(Time duration) override;\n\nprivate:\n    friend AudioEngineAccessor;\n\n    enum class Direction : bool\n    {\n        INPUT = true,\n        OUTPUT = false\n    };\n    /**\n     * @brief Register a newly created processor in all lookup containers\n     *        and take ownership of it.\n     * @param processor Processor to register\n     */\n    EngineReturnStatus _register_processor(std::shared_ptr<Processor> processor, const std::string& name);\n\n    /**\n     * @brief Remove a processor from the engine. The processor must not be active\n     *        on any track when called. The engine does not hold any references\n     *        to the processor after this function has returned.\n     * @param processor A pointer to the instance of the processor to delete\n     */\n    void _deregister_processor(Processor* processor);\n\n    /**\n     * @brief Add a registered processor to the realtime processing part.\n     *        Must be called before a processor can be used to process audio.\n     * @param processor Processor to insert\n     */\n    bool _insert_processor_in_realtime_part(Processor* processor);\n\n    /**\n     * @brief Remove a processor from the realtime processing part\n     *        Must be called before de-registering a processor.\n     * @param name The unique name of the processor to delete\n     * @return True if the processor existed and it was correctly deleted\n     */\n    bool _remove_processor_from_realtime_part(ObjectId processor);\n\n    /**\n     * @brief Remove all audio connections from track\n     * @param track_id The id of the track to remove from\n     */\n    void _remove_connections_from_track(ObjectId track_id);\n\n    /**\n     * @brief Register a newly created track\n     * @param track Pointer to the track\n     * @return OK if successful, error code otherwise\n     */\n    EngineReturnStatus _register_new_track(const std::string& name, std::shared_ptr<Track> track, std::optional<int> thread);\n\n    /**\n    * @brief Called from a non-realtime thread to process a control event in the realtime thread\n    * @param event The event to process\n    * @return EngineReturnStatus::OK if the event was properly processed, error code otherwise\n    */\n\n    std::pair<EngineReturnStatus, ObjectId> _create_master_track(const std::string& name, TrackType type, int channels);\n\n    EngineReturnStatus _send_control_event(RtEvent& event);\n\n    EngineReturnStatus _connect_audio_channel(int engine_channel, int track_channel, ObjectId track_id, Direction direction);\n\n    EngineReturnStatus _disconnect_audio_channel(int engine_channel, int track_channel, ObjectId track_id, Direction direction);\n\n    void _process_internal_rt_events();\n\n    void _send_rt_events_to_processors();\n\n    void _send_rt_event(const RtEvent& event);\n\n    inline void _retrieve_events_from_tracks(ControlBuffer& buffer);\n\n    inline void _retrieve_events_from_output_pipe(RtEventFifo<>& pipe, ControlBuffer& buffer);\n\n    inline void _copy_audio_to_tracks(ChunkSampleBuffer* input);\n\n    inline void _copy_audio_from_tracks(ChunkSampleBuffer* output);\n\n    /**\n     * @brief Add a track to the audio engine, if engine is running, this must be called from the\n     *        rt thread before/after processing. If not running, then this function can safely be\n     *        called from a non-rt thread\n     * @param track The track to add\n     * @return True if successful, false otherwise\n     */\n    bool _add_track(Track* track, std::optional<int> thread);\n\n    /**\n     * @brief Remove a track from the audio engine, if engine is running, this must be called from\n     *        the rt thread before/after processing. If not running, then this function can safely be\n     *        called from a non-rt thread\n     * @param track The track to remove.\n     * @return True if successful, false otherwise\n */\n    bool _remove_track(Track* track);\n\n    void print_timings_to_file(const std::string& filename);\n\n    void _route_cv_gate_ins(ControlBuffer& buffer);\n\n    std::unique_ptr<dispatcher::BaseEventDispatcher> _event_dispatcher;\n\n    PluginRegistry _plugin_registry;\n    ProcessorContainer _processors;\n\n    // Processors in the realtime part indexed by their unique 32 bit id\n    // Only to be accessed from the process callback in rt mode.\n    std::vector<Processor*> _realtime_processors {MAX_RT_PROCESSOR_ID, nullptr};\n    AudioGraph              _audio_graph;\n\n    Track* _pre_track{nullptr};\n    Track* _post_track{nullptr};\n    ChunkSampleBuffer _input_swap_buffer;\n    ChunkSampleBuffer _output_swap_buffer;\n\n    ConnectionStorage<AudioConnection> _audio_in_connections;\n    ConnectionStorage<AudioConnection> _audio_out_connections;\n    std::vector<CvConnection>    _cv_in_connections;\n    std::vector<GateConnection>  _gate_in_connections;\n\n    BitSet32 _prev_gate_values{0};\n    BitSet32 _outgoing_gate_values{0};\n\n    std::atomic<RealtimeState> _state{RealtimeState::STOPPED};\n\n    RtSafeRtEventFifo _control_queue_in;\n    RtSafeRtEventFifo _main_in_queue;\n    RtSafeRtEventFifo _main_out_queue;\n    RtSafeRtEventFifo _control_queue_out;\n    std::mutex _in_queue_lock;\n    RtEventFifo<> _prepost_event_outputs;\n    receiver::AsynchronousEventReceiver _event_receiver{&_control_queue_out};\n    Transport _transport;\n    PluginLibrary _plugin_library;\n\n    HostControl _host_control{nullptr, &_transport, &_plugin_library};\n\n    performance::PerformanceTimer _process_timer;\n    int  _log_timing_print_counter{0};\n\n    bool _input_clip_detection_enabled{false};\n    bool _output_clip_detection_enabled{false};\n    ClipDetector _clip_detector;\n\n    bool _master_limiter_enabled{false};\n    std::vector<sushi::dsp::MasterLimiter<AUDIO_CHUNK_SIZE>> _master_limiters;\n};\n\n/**\n * @brief Helper function to encapsulate state changes from transient states\n * @param current_state The current state of the engine\n * @return A new, non-transient state\n */\nRealtimeState update_state(RealtimeState current_state);\n\n} // end namespace sushi::internal::engine\n\n#endif // SUSHI_ENGINE_H\n"
  },
  {
    "path": "src/engine/audio_graph.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Wrapper around the list of tracks used for rt processing and its associated\n *        multicore management\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"audio_graph.h\"\n\nnamespace sushi::internal::engine {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"audio graph\");\n\nconstexpr bool DISABLE_DENORMALS = true;\n\n/**\n * Real-time worker thread callback method.\n */\nvoid external_render_callback(void* data)\n{\n    auto node = reinterpret_cast<sushi::internal::engine::AudioGraph::GraphNode*>(data);\n    auto start = node->timer->start_timer();\n\n    for (auto track : node->tracks)\n    {\n        track->render();\n    }\n    node->timer->stop_timer(start, (-2 - node->thread_id)); // Thread ids are counted backwards from -2\n}\n\nAudioGraph::AudioGraph(int threads,\n                       int max_no_tracks,\n                       performance::PerformanceTimer* timer,\n                       [[maybe_unused]] float sample_rate,\n                       [[maybe_unused]] std::optional<std::string> device_name,\n                       bool debug_mode_switches) : _audio_graph(threads),\n                                                   _event_outputs(threads),\n                                                   _cores(threads),\n                                                   _current_core(0)\n{\n    assert(threads > 0);\n\n    if (_cores > 1)\n    {\n        twine::apple::AppleMultiThreadData apple_data{};\n#ifdef SUSHI_APPLE_THREADING\n        apple_data.chunk_size = AUDIO_CHUNK_SIZE;\n        apple_data.current_sample_rate = sample_rate;\n        if (device_name.has_value())\n        {\n            apple_data.device_name = device_name.value();\n        }\n#endif\n        int thread_id = 0;\n        _worker_pool = twine::WorkerPool::create_worker_pool(_cores,\n                                                             apple_data,\n                                                             DISABLE_DENORMALS,\n                                                             debug_mode_switches);\n\n        for (auto& node : _audio_graph)\n        {\n\n            auto status = _worker_pool->add_worker(external_render_callback,\n                                                   &node);\n\n            if (status.first != twine::WorkerPoolStatus::OK)\n            {\n#ifdef SUSHI_APPLE_THREADING\n                ELKLOG_LOG_ERROR(\"Failed to start twine worker: {}\", twine::apple::status_to_string(status.second));\n#endif\n            }\n\n            node.tracks.reserve(max_no_tracks);\n            node.timer = timer;\n            node.thread_id = thread_id++;\n        }\n        std::string cpu_ids;\n        for (const auto& info : _worker_pool->core_info())\n        {\n            _core_ids.push_back(info.id);\n            ELKLOG_LOG_WARNING_IF(info.workers > 1, \"Multiple workers assigned to core {}\", info.id);\n            cpu_ids.append(std::to_string(info.id)).append(\", \");\n        }\n        ELKLOG_LOG_INFO(\"Worker pool created with {} cores on cpus: {}\", _core_ids.size(), cpu_ids);\n    }\n    else\n    {\n        _audio_graph[0].tracks.reserve(max_no_tracks);\n        _core_ids.push_back(0);\n    }\n}\n\nbool AudioGraph::add(Track* track)\n{\n    auto& slot = _audio_graph[_current_core].tracks;\n    if (slot.size() < slot.capacity())\n    {\n        track->set_event_output(&_event_outputs[_current_core]);\n        track->set_active_rt_processing(true, _current_core);\n        slot.push_back(track);\n        _current_core = (_current_core + 1) % _cores;\n        return true;\n    }\n    return false;\n}\n\nbool AudioGraph::add_to_thread(Track* track, int thread)\n{\n    assert(thread < _cores);\n    // TODO - Clamp thread or return false to avoid segfault?\n    auto& slot = _audio_graph[thread].tracks;\n    if (slot.size() < slot.capacity())\n    {\n        track->set_event_output(&_event_outputs[thread]);\n        track->set_active_rt_processing(true, thread);\n        slot.push_back(track);\n        return true;\n    }\n    return false;\n}\n\nbool AudioGraph::remove(Track* track)\n{\n    for (auto& node : _audio_graph)\n    {\n        for (auto i = node.tracks.begin(); i != node.tracks.end(); ++i)\n        {\n            if (*i == track)\n            {\n                node.tracks.erase(i);\n                track->set_active_rt_processing(false, 0);\n                return true;\n            }\n        }\n    }\n    return false;\n}\n\nvoid AudioGraph::render()\n{\n    if (_cores == 1)\n    {\n        for (auto& track : _audio_graph[0].tracks)\n        {\n            track->render();\n        }\n    }\n    else\n    {\n        _worker_pool->wakeup_and_wait();\n    }\n}\n\n} // end namespace sushi::internal::engine\n"
  },
  {
    "path": "src/engine/audio_graph.h",
    "content": "#ifndef SUSHI_AUDIO_GRAPH_H\n#define SUSHI_AUDIO_GRAPH_H\n\n/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Wrapper around the list of tracks used for rt processing and its associated\n *        multicore management\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <vector>\n\n#include \"twine/twine.h\"\n\n#include \"engine/track.h\"\n#include \"library/performance_timer.h\"\n\nnamespace sushi::internal::engine {\n\nclass AudioGraphAccessor;\n\nclass AudioGraph\n{\npublic:\n    /**\n     * @brief create an AudioGraph instance\n     * @param threads The number of cores to use for audio processing. Must not\n     *                exceed the number of cores on the architecture\n     * @param max_no_tracks The maximum number of tracks to reserve space for. As\n     *                      add() and remove() could be called from an rt thread\n     *                      they must not (de)allocate memory.\n     * @param timer A timer used to measure the time it takes to process an audio chunk\n     * @param sample_rate The sample_rate - used for calculating audio thread periodicity. Only used on Apple.\n     * @param device_name The Audio Device Name - only used on Apple, and will be unused on other platforms.\n     * @param debug_mode_switches Enable xenomai-specific thread debugging\n     */\n    AudioGraph(int threads,\n               int max_no_tracks,\n               performance::PerformanceTimer* timer,\n               float sample_rate,\n               std::optional<std::string> device_name = std::nullopt,\n               bool debug_mode_switches = false);\n\n    ~AudioGraph() = default;\n\n    /**\n     * @brief Add a track to the graph. The track will be assigned to a cpu\n     *        core on a round robin basis. Must not be called concurrently\n     *        with render()\n     * @param track the track instance to add\n     * @return true if the track was successfully added, false otherwise\n     */\n    bool add(Track* track);\n\n    /**\n     * @brief Add a track to the graph and assign it to a particular audio thread.\n     *        Must not be called concurrently with render()\n     * @param track the track instance to add\n     * @param thread The worker thread that should be used to process the track.\n     *               Threads are numbered from 0 to the maximum number of audio\n     *               processing threads Sushi is configured to run with and does\n     *               not directly correspond to a cpu core.\n     * @return true if the track was successfully added, false otherwise\n     */\n    bool add_to_thread(Track* track, int thread);\n\n    /**\n     * @brief Remove a track from the audio graph. Must not be called concurrently\n     *        with render()\n     * @param track The instance to remove.\n     * @return true if the track was successfully removed, false otherwise.\n     */\n    bool remove(Track* track);\n\n    /**\n     * @brief Return the event output buffers for all tracks. Called after render()\n     *        to retrieve events passed from tracks.\n     * @return A std::vector of event buffers.\n     */\n    std::vector<RtEventFifo<>>& event_outputs()\n    {\n        return _event_outputs;\n    }\n\n    /**\n     * @brief Render all tracks. If cpu_cores = 1 all processing is done in the\n     *        calling thread. With higher number of cores, the calling thread\n     *        sleeps while processing is running.\n     */\n    void render();\n\n    /**\n     * @brief The maximum number of simultaneous audio threads to use\n     * @return The number of audio threads configured to use\n     */\n    [[nodiscard]] int threads() const\n    {\n        return _cores;\n    }\n\nprivate:\n    friend AudioGraphAccessor;\n    friend void external_render_callback(void*);\n\n    struct GraphNode\n    {\n        std::vector<Track*>             tracks;\n        performance::PerformanceTimer*  timer;\n        int                             thread_id;\n    };\n\n    std::vector<GraphNode>             _audio_graph;\n    std::unique_ptr<twine::WorkerPool> _worker_pool;\n    std::vector<RtEventFifo<>>         _event_outputs;\n    std::vector<int>                   _core_ids;\n    int _cores;\n    int _current_core;\n};\n\n} // end namespace sushi::internal::engine\n\n#endif // SUSHI_AUDIO_GRAPH_H\n"
  },
  {
    "path": "src/engine/base_engine.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Real time audio processing engine interface\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_BASE_ENGINE_H\n#define SUSHI_BASE_ENGINE_H\n\n#include <memory>\n#include <map>\n#include <vector>\n#include <utility>\n#include <bitset>\n#include <limits>\n#include <string>\n\n#include \"sushi/constants.h\"\n#include \"sushi/types.h\"\n#include \"sushi/sushi_time.h\"\n#include \"sushi/sample_buffer.h\"\n#include \"sushi/control_interface.h\"\n\n#include \"base_event_dispatcher.h\"\n#include \"base_processor_container.h\"\n#include \"track.h\"\n\n#include \"library/base_performance_timer.h\"\n#include \"library/connection_types.h\"\n\nnamespace sushi::internal::engine {\n\nusing BitSet32 = std::bitset<std::numeric_limits<uint32_t>::digits>;\n\nstruct ControlBuffer\n{\n    ControlBuffer() = default;\n\n    std::array<float, MAX_ENGINE_CV_IO_PORTS> cv_values {{0}};\n    BitSet32 gate_values {0};\n};\n\nenum class EngineReturnStatus\n{\n    OK,\n    ERROR,\n    INVALID_N_CHANNELS,\n    INVALID_PLUGIN_UID,\n    INVALID_PLUGIN,\n    INVALID_PLUGIN_TYPE,\n    INVALID_PROCESSOR,\n    INVALID_PARAMETER,\n    INVALID_TRACK,\n    INVALID_BUS,\n    INVALID_CHANNEL,\n    ALREADY_IN_USE,\n    QUEUE_FULL\n};\n\nenum class RealtimeState\n{\n    STARTING,\n    RUNNING,\n    STOPPING,\n    STOPPED\n};\n\nconstexpr int ENGINE_TIMING_ID = -1;\n\nclass BaseEngine\n{\npublic:\n    explicit BaseEngine(float sample_rate) : _sample_rate(sample_rate)\n    {}\n\n    virtual ~BaseEngine() = default;\n\n    float sample_rate() const\n    {\n        return _sample_rate;\n    }\n\n    virtual void set_sample_rate(float sample_rate)\n    {\n        _sample_rate = sample_rate;\n    }\n\n    virtual void set_audio_channels(int inputs, int outputs)\n    {\n        _audio_inputs = inputs;\n        _audio_outputs = outputs;\n    }\n\n    int audio_input_channels() const\n    {\n        return _audio_inputs;\n    }\n\n    int audio_output_channels() const\n    {\n        return _audio_outputs;\n    }\n\n    virtual EngineReturnStatus set_cv_input_channels(int channels)\n    {\n        _cv_inputs = channels;\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus set_cv_output_channels(int channels)\n    {\n        _cv_outputs = channels;\n        return EngineReturnStatus::OK;\n    }\n\n    int cv_input_channels() const\n    {\n        return _cv_inputs;\n    }\n\n    int cv_output_channels() const\n    {\n        return _cv_outputs;\n    }\n\n    virtual EngineReturnStatus connect_audio_input_channel(int /*engine_channel*/,\n                                                           int /*track_channel*/,\n                                                           ObjectId /*track_id*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus connect_audio_output_channel(int /*engine_channel*/,\n                                                            int /*track_channel*/,\n                                                            ObjectId /*track_id*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus disconnect_audio_input_channel(int /*engine_channel*/,\n                                                              int /*track_channel*/,\n                                                              ObjectId /*track_id*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus disconnect_audio_output_channel(int /*engine_channel*/,\n                                                               int /*track_channel*/,\n                                                               ObjectId /*track_id*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual std::vector<AudioConnection> audio_input_connections()\n    {\n        return {};\n    }\n\n    virtual std::vector<AudioConnection> audio_output_connections()\n    {\n        return {};\n    }\n\n    virtual EngineReturnStatus connect_audio_input_bus(int /*input_bus */,\n                                                       int /*track_bus*/,\n                                                       ObjectId  /*track_id*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus connect_audio_output_bus(int /*output_bus*/,\n                                                        int /*track_bus*/,\n                                                        ObjectId  /*track_id*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus connect_cv_to_parameter(const std::string& /*processor_name*/,\n                                                       const std::string& /*parameter_name*/,\n                                                       int /*cv_input_id*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus connect_cv_from_parameter(const std::string& /*processor_name*/,\n                                                         const std::string& /*parameter_name*/,\n                                                         int /*cv_output_id*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus connect_gate_to_processor(const std::string& /*processor_name*/,\n                                                         int /*gate_input_id*/,\n                                                         int /*note_no*/,\n                                                         int /*channel*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus connect_gate_from_processor(const std::string& /*processor_name*/,\n                                                           int /*gate_output_id*/,\n                                                           int /*note_no*/,\n                                                           int /*channel*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus connect_gate_to_sync(int /*gate_input_id*/,\n                                                    int /*ppq_ticks*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus connect_sync_to_gate(int /*gate_output_id*/,\n                                                    int /*ppq_ticks*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual bool realtime()\n    {\n        return true;\n    }\n\n    virtual void enable_realtime(bool /*enabled*/) {}\n\n    virtual void process_chunk(SampleBuffer<AUDIO_CHUNK_SIZE>* in_buffer,\n                               SampleBuffer<AUDIO_CHUNK_SIZE>* out_buffer,\n                               ControlBuffer* in_controls,\n                               ControlBuffer* out_controls,\n                               Time timestamp,\n                               int64_t samplecount) = 0;\n\n    virtual void set_output_latency(Time /*latency*/) = 0;\n\n    virtual void set_tempo(float /*tempo*/) = 0;\n\n    virtual void set_time_signature(TimeSignature /*signature*/) = 0;\n\n    virtual void set_transport_mode(PlayingMode /*mode*/) = 0;\n\n    virtual void set_tempo_sync_mode(SyncMode /*mode*/) = 0;\n\n    virtual void set_base_plugin_path(const std::string& /*path*/) = 0;\n\n    virtual EngineReturnStatus send_rt_event_to_processor(const RtEvent& /*event*/) = 0;\n\n    virtual std::pair<EngineReturnStatus, ObjectId> create_track(const std::string&, int, std::optional<int>)\n    {\n        return {EngineReturnStatus::OK, 0};\n    }\n\n    virtual std::pair<EngineReturnStatus, ObjectId> create_multibus_track(const std::string&, int, std::optional<int>)\n    {\n        return {EngineReturnStatus::OK, 0};\n    }\n\n    virtual std::pair<EngineReturnStatus, ObjectId> create_post_track(const std::string& /*name*/)\n    {\n        return {EngineReturnStatus::OK, 0};\n    }\n\n    virtual std::pair<EngineReturnStatus, ObjectId> create_pre_track(const std::string& /*name*/)\n    {\n        return {EngineReturnStatus::OK, 0};\n    }\n\n    virtual EngineReturnStatus delete_track(ObjectId  /*track_id*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual std::pair <EngineReturnStatus, ObjectId> create_processor(const PluginInfo& /*plugin_info*/,\n                                                                      const std::string& /*processor_name*/)\n    {\n        return {EngineReturnStatus::OK, ObjectId(0)};\n    }\n\n    virtual EngineReturnStatus add_plugin_to_track(ObjectId /*plugin_id*/,\n                                                   ObjectId /*track_id*/,\n                                                   std::optional<ObjectId> /*before_plugin_id*/ = std::nullopt)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus remove_plugin_from_track(ObjectId /*plugin_id*/,\n                                                        ObjectId /*track_id*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual EngineReturnStatus delete_plugin(ObjectId /*plugin_id*/)\n    {\n        return EngineReturnStatus::OK;\n    }\n\n    virtual dispatcher::BaseEventDispatcher* event_dispatcher()\n    {\n        return nullptr;\n    }\n\n    virtual engine::Transport* transport()\n    {\n        return nullptr;\n    }\n\n    virtual performance::BasePerformanceTimer* performance_timer()\n    {\n        return nullptr;\n    }\n\n    virtual const BaseProcessorContainer* processor_container()\n    {\n        return nullptr;\n    }\n\n    virtual void enable_input_clip_detection(bool /*enabled*/) {}\n\n    virtual void enable_output_clip_detection(bool /*enabled*/) {}\n\n    virtual bool input_clip_detection() const {return false;}\n\n    virtual bool output_clip_detection() const {return false;}\n\n    virtual void enable_master_limiter(bool /*enabled*/) {}\n\n    virtual bool master_limiter() const {return false;}\n\n    virtual void update_timings() {}\n\n    virtual void notify_interrupted_audio(Time /*duration*/) {}\n\nprotected:\n    float _sample_rate;\n    int _audio_inputs{0};\n    int _audio_outputs{0};\n    int _cv_inputs{0};\n    int _cv_outputs{0};\n};\n\n} // end namespace sushi::internal::engine\n\n#endif // SUSHI_BASE_ENGINE_H\n"
  },
  {
    "path": "src/engine/base_event_dispatcher.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Main event handler in Sushi and responsible for conversion between\n *        regular and rt events.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_BASE_EVENT_DISPATCHER_H\n#define SUSHI_BASE_EVENT_DISPATCHER_H\n\n#include \"library/event.h\"\n#include \"library/event_interface.h\"\n\nnamespace sushi::internal::dispatcher {\n\nenum class Status\n{\n    OK,\n    ALREADY_SUBSCRIBED,\n    UNKNOWN_POSTER\n};\n\n/* Abstract base class is solely for test mockups */\nclass BaseEventDispatcher\n{\npublic:\n    virtual ~BaseEventDispatcher() = default;\n\n    virtual void run() = 0;\n    virtual void stop() = 0;\n\n    virtual void post_event(std::unique_ptr<Event> event) = 0;\n\n    virtual Status subscribe_to_keyboard_events(EventPoster* /*receiver*/)\n    {\n        return Status::OK;\n    }\n\n    virtual Status subscribe_to_parameter_change_notifications(EventPoster* /*receiver*/)\n    {\n        return Status::OK;\n    }\n\n    virtual Status subscribe_to_engine_notifications(EventPoster* /*receiver*/)\n    {\n        return Status::OK;\n    }\n\n    virtual Status unsubscribe_from_keyboard_events(EventPoster* /*receiver*/)\n    {\n        return Status::OK;\n    }\n\n    virtual Status unsubscribe_from_parameter_change_notifications(EventPoster* /*receiver*/)\n    {\n        return Status::OK;\n    }\n\n    virtual Status unsubscribe_from_engine_notifications(EventPoster* /*receiver*/)\n    {\n        return Status::OK;\n    }\n\n    virtual void set_sample_rate(float /*sample_rate*/) = 0;\n    virtual void set_time(Time /*timestamp*/)  = 0;\n\n    virtual int dispatch(std::unique_ptr<Event> /*event*/) = 0;\n};\n\n} // end namespace sushi::internal::dispatcher\n\n#endif // SUSHI_BASE_EVENT_DISPATCHER_H\n"
  },
  {
    "path": "src/engine/base_processor_container.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Container interface for audio processors\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_BASE_PROCESSOR_CONTAINER_H\n#define SUSHI_BASE_PROCESSOR_CONTAINER_H\n\n#include <memory>\n#include <vector>\n\n#include \"sushi/constants.h\"\n\n#include \"library/processor.h\"\n#include \"engine/track.h\"\n\nnamespace sushi::internal::engine {\n\nclass BaseProcessorContainer\n{\npublic:\n    virtual ~BaseProcessorContainer() = default;\n\n    virtual bool add_processor(std::shared_ptr<Processor> track) = 0;\n\n    virtual bool add_track(std::shared_ptr<Track> track) = 0;\n\n    virtual bool remove_processor(ObjectId id) = 0;\n\n    virtual bool remove_track(ObjectId track_id) = 0;\n\n    virtual bool add_to_track(std::shared_ptr<Processor> processor, ObjectId track_id, std::optional<ObjectId> before_id) = 0;\n\n    virtual bool remove_from_track(ObjectId processor_id, ObjectId track_id) = 0;\n\n    virtual bool processor_exists(ObjectId id) const = 0;\n\n    virtual bool processor_exists(const std::string& name) const = 0;\n\n    virtual std::vector<std::shared_ptr<const Processor>> all_processors() const = 0;\n\n    virtual std::shared_ptr<Processor> mutable_processor(ObjectId id) const = 0;\n\n    virtual std::shared_ptr<Processor> mutable_processor(const std::string& name) const = 0;\n\n    virtual std::shared_ptr<const Processor> processor(ObjectId) const = 0;\n\n    virtual std::shared_ptr<const Processor> processor(const std::string& name) const = 0;\n\n    virtual std::shared_ptr<Track> mutable_track(ObjectId track_id) const = 0;\n\n    virtual std::shared_ptr<Track> mutable_track(const std::string& track_name) const = 0;\n\n    virtual std::shared_ptr<const Track> track(ObjectId track_id) const = 0;\n\n    virtual std::shared_ptr<const Track> track(const std::string& name) const = 0;\n\n    virtual std::vector<std::shared_ptr<const Processor>> processors_on_track(ObjectId track_id) const = 0;\n\n    virtual std::vector<std::shared_ptr<const Track>> all_tracks() const = 0;\n\nprotected:\n    BaseProcessorContainer() = default;\n};\n\n} // end namespace sushi::internal::engine\n\n#endif // SUSHI_BASE_PROCESSOR_CONTAINER_H\n"
  },
  {
    "path": "src/engine/connection_storage.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Class for wrapping the 2 data sets (rt and non-rt) needed for realtime\n *        safe operation of Audio and Cv/Gate connections\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_CONNECTION_ROUTER_H\n#define SUSHI_CONNECTION_ROUTER_H\n\n#include <vector>\n#include <mutex>\n#include <type_traits>\n#include <cassert>\n\n#include \"twine/twine.h\"\n\n#include \"library/rt_event_pipe.h\"\n\nnamespace sushi::internal {\n\ntemplate <typename T>\nclass ConnectionStorage\n{\npublic:\n    explicit ConnectionStorage(int max_connections) : _capacity(max_connections)\n    {\n        _items_rt.reserve(max_connections);\n    }\n\n    /**\n     * @brief Get the current items, called from rt threads only, should not be called\n     *        be called concurrently with add_rt()\n     * @return A reference to a vector of elements in the container\n     */\n    const std::vector<T>& connections_rt() const\n    {\n        assert(twine::is_current_thread_realtime());\n        return _items_rt;\n    }\n\n    /**\n     * @brief Get the current elements, called from not-rt threads only. Returns a copy of the\n     *        items currently in the container\n     * @return A vector of elements in the container\n     */\n    std::vector<T> connections() const\n    {\n        assert(twine::is_current_thread_realtime() == false);\n        std::scoped_lock<std::mutex> lock(_non_rt_lock);\n        auto copy = _items;\n        return copy;\n    }\n\n    /**\n     * @brief Add an element to the container. Should only be called from a non-rt thread\n     * @param element   The element to add to the container.\n     * @param add_to_rt If true, also adds the element to the rt-part of the container.\n     *                  This should only be set to true if there are no concurrent calls from a\n     *                  rt thread to the container. If set to false, add_rt() needs to be called\n     *                  from an rt thread afterwards.\n     * @return True if the element was successfully added, false if the max capacity was reached.\n     */\n    bool add(const T& element, bool add_to_rt)\n    {\n        assert(twine::is_current_thread_realtime() == false);\n        std::scoped_lock<std::mutex> lock(_non_rt_lock);\n        if (_items.size() < _capacity)\n        {\n            _items.push_back(element);\n            if (add_to_rt)\n            {\n                _items_rt.push_back(element);\n            }\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * @brief Add an element to the rt part of the container. Should only be called from an rt thread\n     * @param element   The element to add to the container.\n     * @return True if the element was successfully added, false if the max capacity was reached.\n     */\n    bool add_rt(const T& element)\n    {\n        assert(twine::is_current_thread_realtime());\n        assert(_items_rt.size() < _capacity);\n        _items_rt.push_back(element);\n        return true;\n    }\n\n    /**\n     * @brief Remove an element from the container. Should only be called from a non-rt thread\n     * @param pattern   Elements matching this pattern will be removed.\n     * @param remove_from_rt If true, also removes the element from the rt-part of the container.\n     *                       This should only be set to true if there are no concurrent calls from a\n     *                       rt thread to the container. If set to false, remove_rt() needs to be\n     *                       called from an rt thread afterwards.\n     * @return True if the element was found and successfully deleted.\n     */\n    bool remove(const T& pattern, bool remove_from_rt)\n    {\n        assert(twine::is_current_thread_realtime() == false);\n        std::scoped_lock<std::mutex> lock(_non_rt_lock);\n        auto original_size = _items.size();\n        _items.erase(std::remove(_items.begin(), _items.end(), pattern), _items.end());\n        if (remove_from_rt)\n        {\n            _items_rt.erase(std::remove(_items_rt.begin(), _items_rt.end(), pattern), _items_rt.end());\n        }\n        return (original_size != _items.size());\n    }\n\n    /**\n     * @brief Remove an element from the rt part of the container. Should only be called from an rt thread\n     * @param pattern   Elements matching this pattern will be removed.\n     * @return True if the element was found and successfully deleted.\n     */\n    bool remove_rt(const T& pattern)\n    {\n        assert(twine::is_current_thread_realtime());\n        auto original_size = _items_rt.size();\n        _items_rt.erase(std::remove(_items_rt.begin(), _items_rt.end(), pattern), _items_rt.end());\n        return (original_size != _items_rt.size());\n    }\n\n    size_t capacity() const\n    {\n        return _capacity;\n    }\n\nprivate:\n    // For the mutable rt-operations to be guaranteed rt-safe (no allocations) these must hold\n    static_assert(std::is_nothrow_copy_constructible<T>::value);\n    static_assert(std::is_nothrow_destructible<T>::value);\n\n    std::vector<T>      _items;\n    std::vector<T>      _items_rt;\n    size_t              _capacity;\n    mutable std::mutex  _non_rt_lock;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_CONNECTION_ROUTER_H\n"
  },
  {
    "path": "src/engine/controller/audio_graph_controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"audio_graph_controller.h\"\n#include \"completion_sender.h\"\n#include \"library/processor_state.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"controller\");\n\nnamespace sushi::internal::engine::controller_impl {\n\ninline control::ProcessorInfo to_external(const Processor* proc)\n{\n    return control::ProcessorInfo{.id = static_cast<int>(proc->id()),\n                                  .label = proc->label(),\n                                  .name = proc->name(),\n                                  .parameter_count = proc->parameter_count(),\n                                  .program_count = proc->supports_programs()? proc->program_count() : 0};\n}\n\ninline control::TrackInfo to_external(const Track* track, std::vector<int> proc_ids)\n{\n    return control::TrackInfo{.id = static_cast<int>(track->id()),\n                              .label = track->label(),\n                              .name = track->name(),\n                              .channels = track->input_channels(),\n                              .buses = track->buses(),\n                              .thread = track->thread(),\n                              .type = to_external(track->type()),\n                              .processors = std::move(proc_ids)};\n}\n\nAudioGraphController::AudioGraphController(BaseEngine* engine,\n                                           CompletionSender* sender) : _engine(engine),\n                                                                       _processors(engine->processor_container()),\n                                                                       _sender(sender)\n{}\n\nstd::vector<control::ProcessorInfo> AudioGraphController::get_all_processors() const\n{\n    ELKLOG_LOG_DEBUG(\"get_all_processors called\");\n    auto processors = _processors->all_processors();\n    std::vector<control::ProcessorInfo> returns;\n    returns.reserve(processors.size());\n\n    for (const auto& p : processors)\n    {\n        returns.push_back(to_external(p.get()));\n    }\n    return returns;\n}\n\nstd::vector<control::TrackInfo> AudioGraphController::get_all_tracks() const\n{\n    ELKLOG_LOG_DEBUG(\"get_tracks called\");\n    auto tracks = _processors->all_tracks();\n    std::vector<control::TrackInfo> returns;\n    returns.reserve(tracks.size());\n\n    for (const auto& t : tracks)\n    {\n        returns.push_back(to_external(t.get(), _get_processor_ids(t->id())));\n    }\n    return returns;\n}\n\nstd::pair<control::ControlStatus, int> AudioGraphController::get_track_id(const std::string& track_name) const\n{\n    ELKLOG_LOG_DEBUG(\"get_track_id called with track {}\", track_name);\n\n    auto track = _processors->track(track_name);\n    if (track)\n    {\n        return {control::ControlStatus::OK, track->id()};\n    }\n    return {control::ControlStatus::NOT_FOUND, 0};\n}\n\nstd::pair<control::ControlStatus, control::TrackInfo> AudioGraphController::get_track_info(int track_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_track_info called with track {}\", track_id);\n    control::TrackInfo info;\n    const auto& tracks = _processors->all_tracks();\n    auto track = std::find_if(tracks.cbegin(), tracks.cend(),\n                              [&] (const auto& t) {return t->id() == static_cast<ObjectId>(track_id);});\n\n    if (track != tracks.cend())\n    {\n        return {control::ControlStatus::OK, to_external((*track).get(), _get_processor_ids((*track)->id()))};\n    }\n\n    return {control::ControlStatus::NOT_FOUND, info};\n}\n\nstd::pair<control::ControlStatus, std::vector<control::ProcessorInfo>> AudioGraphController::get_track_processors(int track_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_track_processors called for track: {}\", track_id);\n    const auto& tracks = _processors->processors_on_track(track_id);\n    std::vector<control::ProcessorInfo> infos;\n    if (tracks.empty() && _processors->processor_exists(track_id) == false)\n    {\n        return {control::ControlStatus::NOT_FOUND, infos};\n    }\n    for (const auto& processor : tracks)\n    {\n        infos.push_back(to_external(processor.get()));\n    }\n    return {control::ControlStatus::OK, infos};\n}\n\nstd::pair<control::ControlStatus, int> AudioGraphController::get_processor_id(const std::string& processor_name) const\n{\n    ELKLOG_LOG_DEBUG(\"get_processor_id called with processor {}\", processor_name);\n    auto processor = _processors->processor(processor_name);\n    if (processor)\n    {\n        return {control::ControlStatus::OK, processor->id()};\n    }\n    return {control::ControlStatus::NOT_FOUND, 0};\n}\n\nstd::pair<control::ControlStatus, control::ProcessorInfo> AudioGraphController::get_processor_info(int processor_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_processor_info called with processor {}\", processor_id);\n\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor)\n    {\n        return {control::ControlStatus::OK, to_external(processor.get())};\n    }\n    return {control::ControlStatus::NOT_FOUND, control::ProcessorInfo()};\n}\n\nstd::pair<control::ControlStatus, bool> AudioGraphController::get_processor_bypass_state(int processor_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_processor_bypass_state called with processor {}\", processor_id);\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor)\n    {\n        return {control::ControlStatus::OK, processor->bypassed()};\n    }\n    return {control::ControlStatus::NOT_FOUND, false};\n}\n\nstd::pair<control::ControlStatus, control::ProcessorState> AudioGraphController::get_processor_state(int processor_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_processor_state called with processor {}\", processor_id);\n    control::ProcessorState state;\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor)\n    {\n        state.bypassed = processor->bypassed();\n        if (processor->supports_programs())\n        {\n            state.program = processor->current_program();\n        }\n        auto params = processor->all_parameters();\n        for (const auto& param : params)\n        {\n            if (param->type() == ParameterType::STRING)\n            {\n                state.properties.push_back({static_cast<int>(param->id()), processor->property_value(param->id()).second});\n            }\n            else\n            {\n                state.parameters.push_back({static_cast<int>(param->id()), processor->parameter_value(param->id()).second});\n            }\n        }\n        return {control::ControlStatus::OK, state};\n    }\n    return {control::ControlStatus::NOT_FOUND, state};\n}\n\ncontrol::ControlResponse AudioGraphController::set_processor_state(int processor_id, const control::ProcessorState& state)\n{\n    ELKLOG_LOG_DEBUG(\"set_processor_state called with processor id {}\", processor_id);\n    auto internal_state = std::make_unique<ProcessorState>();\n    to_internal(internal_state.get(), &state);\n\n    // Capture locals by copy, this via pointer and internal_state by move.\n    auto lambda = [=, this, state = std::move(internal_state)] () -> int\n    {\n        bool realtime = _engine->realtime();\n\n        auto processor = _processors->mutable_processor(static_cast<ObjectId>(processor_id));\n        if (processor)\n        {\n            ELKLOG_LOG_DEBUG(\"Setting state on processor {} with realtime {}\", processor->name(), realtime? \"enabled\" : \"disabled\");\n            processor->set_state(state.get(), realtime);\n            return EventStatus::HANDLED_OK;\n        }\n        ELKLOG_LOG_ERROR(\"Processor {} not found\", processor_id);\n        return ControlEventStatus::NOT_FOUND;\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioGraphController::set_processor_bypass_state(int processor_id, bool bypass_enabled)\n{\n    ELKLOG_LOG_DEBUG(\"set_processor_bypass_state called with {} and processor {}\", bypass_enabled, processor_id);\n    auto processor = _processors->mutable_processor(static_cast<ObjectId>(processor_id));\n    if (processor)\n    {\n        processor->set_bypassed(bypass_enabled);\n        return {control::ControlStatus::OK, 0};\n    }\n    return {control::ControlStatus::NOT_FOUND, 0};\n}\n\ncontrol::ControlResponse AudioGraphController::create_track(const std::string& name, int channels, std::optional<int> thread)\n{\n    ELKLOG_LOG_DEBUG(\"create_track called with name {} and {} channels\", name, channels);\n\n    auto lambda = [=, this] () -> int\n    {\n      auto [status, track_id] = _engine->create_track(name, channels, thread);\n      return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioGraphController::create_multibus_track(const std::string& name, int buses, std::optional<int> thread)\n{\n    ELKLOG_LOG_DEBUG(\"create_multibus_track called with name {} and {} buses \", name, buses);\n    auto lambda = [=, this] () -> int\n    {\n        auto [status, track_id] = _engine->create_multibus_track(name, buses, thread);\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioGraphController::create_pre_track(const std::string& name)\n{\n    ELKLOG_LOG_DEBUG(\"create_pre_track called with name {}\", name);\n    auto lambda = [=, this] () -> int\n    {\n        auto [status, track_id] = _engine->create_pre_track(name);\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioGraphController::create_post_track(const std::string& name)\n{\n    ELKLOG_LOG_DEBUG(\"create_post_track called with name {}\", name);\n    auto lambda = [=, this]() -> int {\n        auto [status, track_id] = _engine->create_post_track(name);\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioGraphController::move_processor_on_track(int processor_id,\n                                                                       int source_track_id,\n                                                                       int dest_track_id,\n                                                                       std::optional<int> before_processor_id)\n{\n    ELKLOG_LOG_DEBUG(\"move_processor_on_track called with processor id {}, source track id and {} dest track id {}\",\n                     processor_id, source_track_id, dest_track_id);\n    auto lambda = [=, this] () -> int\n    {\n        auto plugin_order = _processors->processors_on_track(source_track_id);\n\n        if (_processors->processor_exists(dest_track_id) == false ||\n            plugin_order.empty())\n        {\n            // Normally controllers aren't supposed to do this kind of pre-check as is results in\n            // double look-ups of Processor and Track objects. But given the amount of work needed to\n            // restore a failed insertion, this is justified in this case\n            ELKLOG_LOG_ERROR(\"Processor or dest track not found\");\n            return ControlEventStatus::NOT_FOUND;\n        }\n\n        auto status = _engine->remove_plugin_from_track(processor_id, source_track_id);\n        if (status != EngineReturnStatus::OK)\n        {\n            return map_status(status);\n        }\n\n        status = _engine->add_plugin_to_track(processor_id, dest_track_id, before_processor_id);\n        if (status != engine::EngineReturnStatus::OK)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to move processor {} from track {} to track {} with error {}, reverting\",\n                             processor_id, source_track_id, dest_track_id, static_cast<int>(status));\n\n            // If the insertion operation failed, we must put the processor back in the source track\n            std::optional<ObjectId> position(std::nullopt);\n            if (plugin_order.back()->id() != static_cast<ObjectId>(processor_id))\n            {\n                // plugin position should be the id of the plugin that originally came after\n                auto p = std::find_if(plugin_order.cbegin(), plugin_order.cend(),\n                                      [&] (const auto& p) {return p->id() == static_cast<ObjectId>(processor_id);});\n                position = std::optional<ObjectId>((*p++)->id());\n            }\n\n            [[maybe_unused]] auto replace_status = _engine->add_plugin_to_track(processor_id, source_track_id, position);\n            ELKLOG_LOG_WARNING_IF(replace_status != engine::EngineReturnStatus::OK,\n                                 \"Failed to replace processor {} on track {}\", processor_id, source_track_id)\n\n        }\n        return status == EngineReturnStatus::OK? EventStatus::HANDLED_OK : EventStatus::ERROR;\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioGraphController::create_processor_on_track(const std::string& name,\n                                                                         const std::string& uid,\n                                                                         const std::string& file,\n                                                                         control::PluginType type,\n                                                                         int track_id,\n                                                                         std::optional<int> before_processor_id)\n\n{\n    ELKLOG_LOG_DEBUG(\"create_processor_on_track called with name {}, uid {} from {} on track {}\",\n                                                                    name, uid, file, track_id);\n    auto lambda = [=, this] () -> int\n    {\n        PluginInfo plugin_info;\n        plugin_info.uid = uid;\n        plugin_info.path = file;\n        plugin_info.type = to_internal(type);\n\n        auto [status, plugin_id] = _engine->create_processor(plugin_info, name);\n        if (status != EngineReturnStatus::OK)\n        {\n            return ControlEventStatus::ERROR;\n        }\n\n        ELKLOG_LOG_DEBUG(\"Adding plugin {} to track {}\", name, track_id);\n        status = _engine->add_plugin_to_track(plugin_id, track_id, before_processor_id);\n        if (status != engine::EngineReturnStatus::OK)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to load plugin {} to track {}, destroying plugin\", plugin_id, track_id);\n            _engine->delete_plugin(plugin_id);\n        }\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioGraphController::delete_processor_from_track(int processor_id, int track_id)\n{\n    ELKLOG_LOG_DEBUG(\"delete processor_from_track called with processor id {} and track id {}\",\n                    processor_id, track_id);\n    auto lambda = [=, this] () -> int\n    {\n        auto status = _engine->remove_plugin_from_track(processor_id, track_id);\n        if (status == engine::EngineReturnStatus::OK)\n        {\n            status = _engine->delete_plugin(processor_id);\n        }\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioGraphController::delete_track(int track_id)\n{\n    ELKLOG_LOG_DEBUG(\"delete_track called with id {}\", track_id);\n    auto lambda = [=, this] () -> int\n    {\n        auto track = _processors->track(track_id);\n        if (track == nullptr)\n        {\n            return ControlEventStatus::NOT_FOUND;\n        }\n        auto processors = _processors->processors_on_track(track_id);\n\n        // Remove processors starting with the last, more efficient\n        for (auto p = processors.rbegin(); p != processors.rend(); ++p)\n        {\n            ELKLOG_LOG_DEBUG(\"Removing plugin {} from track: {}\", (*p)->name(), track->name());\n            auto status = _engine->remove_plugin_from_track((*p)->id(), track_id);\n            if (status == engine::EngineReturnStatus::OK)\n            {\n                status = _engine->delete_plugin((*p)->id());\n            }\n            if (status != engine::EngineReturnStatus::OK)\n            {\n                ELKLOG_LOG_ERROR(\"Failed to remove plugin {} from track {}\", (*p)->name(), track->name());\n            }\n        }\n        auto status = _engine->delete_track(track_id);\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\nstd::vector<int> AudioGraphController::_get_processor_ids(int track_id) const\n{\n    std::vector<int> ids;\n    auto processors = _processors->processors_on_track(ObjectId(track_id));\n    ids.reserve(processors.size());\n    for (const auto& p : processors)\n    {\n        ids.push_back(p->id());\n    }\n    return ids;\n}\n\n} // end namespace sushi::internal::engine::controller_impl\n\n"
  },
  {
    "path": "src/engine/controller/audio_graph_controller.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_AUDIO_GRAPH_CONTROLLER_H\n#define SUSHI_AUDIO_GRAPH_CONTROLLER_H\n\n#include \"sushi/control_interface.h\"\n\n#include \"engine/base_engine.h\"\n#include \"engine/base_event_dispatcher.h\"\n#include \"controller_common.h\"\n\nnamespace sushi::internal::engine { class CompletionSender;}\n\nnamespace sushi::internal::engine::controller_impl {\n\nclass AudioGraphController : public control::AudioGraphController\n{\npublic:\n    explicit AudioGraphController(BaseEngine* engine, CompletionSender* sender);\n\n    ~AudioGraphController() override = default;\n\n    std::vector<control::ProcessorInfo> get_all_processors() const override;\n\n    std::vector<control::TrackInfo> get_all_tracks() const override;\n\n    std::pair<control::ControlStatus, int> get_track_id(const std::string& track_name) const override;\n\n    std::pair<control::ControlStatus, control::TrackInfo> get_track_info(int track_id) const override;\n\n    std::pair<control::ControlStatus, std::vector<control::ProcessorInfo>> get_track_processors(int track_id) const override;\n\n    std::pair<control::ControlStatus, int> get_processor_id(const std::string& processor_name) const override;\n\n    std::pair<control::ControlStatus, control::ProcessorInfo> get_processor_info(int processor_id) const override;\n\n    std::pair<control::ControlStatus, bool> get_processor_bypass_state(int processor_id) const override;\n\n    std::pair<control::ControlStatus, control::ProcessorState> get_processor_state(int processor_id) const override;\n\n    control::ControlResponse set_processor_bypass_state(int processor_id, bool bypass_enabled) override;\n\n    control::ControlResponse set_processor_state(int processor_id, const control::ProcessorState& state) override;\n\n    control::ControlResponse create_track(const std::string& name, int channels, std::optional<int> thread) override;\n\n    control::ControlResponse create_multibus_track(const std::string& name, int buses, std::optional<int> thread) override;\n\n    control::ControlResponse create_pre_track(const std::string& name) override;\n\n    control::ControlResponse create_post_track(const std::string& name) override;\n\n    control::ControlResponse move_processor_on_track(int processor_id,\n                                                     int source_track_id,\n                                                     int dest_track_id,\n                                                     std::optional<int> before_processor) override;\n\n    control::ControlResponse create_processor_on_track(const std::string& name,\n                                                       const std::string& uid,\n                                                       const std::string& file,\n                                                       control::PluginType type,\n                                                       int track_id,\n                                                       std::optional<int> before_processor_id) override;\n\n    control::ControlResponse delete_processor_from_track(int processor_id, int track_id) override;\n\n    control::ControlResponse delete_track(int track_id) override;\n\n\nprivate:\n    std::vector<int> _get_processor_ids(int track_id) const;\n\n    engine::BaseEngine*                     _engine;\n    const engine::BaseProcessorContainer*   _processors;\n    CompletionSender*                       _sender;\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif // SUSHI_AUDIO_GRAPH_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/audio_routing_controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"audio_routing_controller.h\"\n#include \"controller_common.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"controller\");\n\nnamespace sushi::internal::engine::controller_impl {\n\ninline control::AudioConnection to_external(const AudioConnection& con)\n{\n    return control::AudioConnection{.track_id = static_cast<int>(con.track),\n                                    .track_channel = con.track_channel,\n                                    .engine_channel = con.engine_channel};\n}\n\nstd::vector<control::AudioConnection> AudioRoutingController::get_all_input_connections() const\n{\n    ELKLOG_LOG_DEBUG(\"get_all_input_connections called\");\n    auto connections = _engine->audio_input_connections();\n    std::vector<control::AudioConnection> returns;\n    returns.reserve(connections.size());\n    for (const auto& connection : connections)\n    {\n        returns.push_back(to_external(connection));\n    }\n    return returns;\n}\n\nstd::vector<control::AudioConnection> AudioRoutingController::get_all_output_connections() const\n{\n    ELKLOG_LOG_DEBUG(\"get_all_output_connections called\");\n    auto connections = _engine->audio_output_connections();\n    std::vector<control::AudioConnection> returns;\n    returns.reserve(connections.size());\n    for (const auto& connection : connections)\n    {\n        returns.push_back(to_external(connection));\n    }\n    return returns;\n}\n\nstd::pair<control::ControlStatus, std::vector<control::AudioConnection>> AudioRoutingController::get_input_connections_for_track(int track_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_input_connections_for_track called with track id {}\", track_id);\n    std::pair<control::ControlStatus, std::vector<control::AudioConnection>> returns;\n    returns.first = control::ControlStatus::OK;\n\n    if (!_engine->processor_container()->processor_exists(track_id))\n    {\n        returns.first = control::ControlStatus::NOT_FOUND;\n        return returns;\n    }\n\n    auto connections = _engine->audio_input_connections();\n    for (const auto& connection : connections)\n    {\n        if (connection.track == static_cast<ObjectId>(track_id))\n        {\n            returns.second.push_back(to_external(connection));\n        }\n    }\n    return returns;\n}\n\nstd::pair<control::ControlStatus, std::vector<control::AudioConnection>> AudioRoutingController::get_output_connections_for_track(int track_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_output_connections_for_track called with track id {}\", track_id);\n    std::pair<control::ControlStatus, std::vector<control::AudioConnection>> returns;\n    returns.first = control::ControlStatus::OK;\n\n    if (!_engine->processor_container()->processor_exists(track_id))\n    {\n        returns.first = control::ControlStatus::NOT_FOUND;\n        return returns;\n    }\n\n    auto connections = _engine->audio_output_connections();\n    for (const auto& connection : connections)\n    {\n        if (connection.track == static_cast<ObjectId>(track_id))\n        {\n            returns.second.push_back(to_external(connection));\n        }\n    }\n    return returns;\n}\n\ncontrol::ControlResponse AudioRoutingController::connect_input_channel_to_track(int track_id, int track_channel, int input_channel)\n{\n    ELKLOG_LOG_DEBUG(\"disconnect_output called with track id {}, track_channel {}, input_channel {}\", track_id, track_channel, input_channel);\n    auto lambda = [=, this] () -> int\n    {\n        auto status = _engine->connect_audio_input_channel(input_channel, track_channel, track_id);\n        ELKLOG_LOG_ERROR_IF(status != EngineReturnStatus::OK, \"Connecting audio channel {} to channel {} of track id {} failed with error {}\",\n                input_channel, track_channel, track_id, static_cast<int>(status))\n\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioRoutingController::connect_output_channel_to_track(int track_id, int track_channel, int output_channel)\n{\n    ELKLOG_LOG_DEBUG(\"connect_output called with track id {}, track_channel {}, output_channel {}\", track_id, track_channel, output_channel);\n    auto lambda = [=, this] () -> int\n    {\n        auto status = _engine->connect_audio_output_channel(output_channel, track_channel, track_id);\n        ELKLOG_LOG_ERROR_IF(status != EngineReturnStatus::OK, \"Connecting audio channel {} from channel {} of track id {} failed with error {}\",\n                           output_channel, track_channel, track_id, static_cast<int>(status))\n\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioRoutingController::disconnect_input(int track_id, int track_channel, int input_channel)\n{\n    ELKLOG_LOG_DEBUG(\"disconnect_input called with track id {}, track_channel {}, input_channel {}\", track_id, track_channel, input_channel);\n    auto lambda = [=, this] () -> int\n    {\n        auto status = _engine->disconnect_audio_input_channel(input_channel, track_channel, track_id);\n        ELKLOG_LOG_ERROR_IF(status != EngineReturnStatus::OK, \"Disconnecting audio channel {} to channel {} of track id {} failed with error {}\",\n                           input_channel, track_channel, track_id, static_cast<int>(status))\n\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioRoutingController::disconnect_output(int track_id, int track_channel, int output_channel)\n{\n    ELKLOG_LOG_DEBUG(\"disconnect_output called with track id {}, track_channel {}, output_channel {}\", track_id, track_channel, output_channel);\n    auto lambda = [=, this] () -> int\n    {\n        auto status = _engine->disconnect_audio_output_channel(output_channel, track_channel, track_id);\n        ELKLOG_LOG_ERROR_IF(status != EngineReturnStatus::OK, \"Disconnecting audio channel {} from channel {} of track id {} failed with error {}\",\n                           output_channel, track_channel, track_id, static_cast<int>(status))\n\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioRoutingController::disconnect_all_inputs_from_track(int track_id)\n{\n    ELKLOG_LOG_DEBUG(\"disconnect_all_inputs_from_track called with track {}\", track_id);\n    auto lambda = [=, this] () -> int\n    {\n        auto connections = _engine->audio_input_connections();\n        for (const auto& connection : connections)\n        {\n            if (connection.track == static_cast<ObjectId>(track_id))\n            {\n                auto status = _engine->disconnect_audio_input_channel(connection.engine_channel,\n                                                                      connection.track_channel,\n                                                                      connection.track);\n                ELKLOG_LOG_ERROR_IF(status != EngineReturnStatus::OK, \"Disconnecting audio channel {} from channel {} of track id {} failed with error {}\",\n                                   connection.engine_channel, connection.track_channel, connection.track, static_cast<int>(status))\n\n                return map_status(status);\n            }\n        }\n        return EventStatus::HANDLED_OK;\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse AudioRoutingController::disconnect_all_outputs_from_track(int track_id)\n{\n    ELKLOG_LOG_DEBUG(\"disconnect_all_outputs_from_track called with track {}\", track_id);\n    auto lambda = [=, this] () -> int\n    {\n        auto connections = _engine->audio_output_connections();\n        for (const auto& connection : connections)\n        {\n            if (connection.track == static_cast<ObjectId>(track_id))\n            {\n                auto status = _engine->disconnect_audio_output_channel(connection.engine_channel,\n                                                                       connection.track_channel,\n                                                                       connection.track);\n                ELKLOG_LOG_ERROR_IF(status != EngineReturnStatus::OK, \"Disconnecting audio channel {} from channel {} of track id {} failed with error {}\",\n                                   connection.engine_channel, connection.track_channel, connection.track, static_cast<int>(status))\n\n                if (status != EngineReturnStatus::OK)\n                {\n                    return map_status(status);\n                }\n            }\n        }\n        return EventStatus::HANDLED_OK;\n    };\n    \n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\n} // end namespace sushi::engine::controller_impl"
  },
  {
    "path": "src/engine/controller/audio_routing_controller.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_AUDIO_ROUTING_CONTROLLER_H\n#define SUSHI_AUDIO_ROUTING_CONTROLLER_H\n\n#include \"sushi/control_interface.h\"\n#include \"engine/base_engine.h\"\n#include \"engine/base_event_dispatcher.h\"\n#include \"completion_sender.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nclass AudioRoutingController : public control::AudioRoutingController\n{\npublic:\n    explicit AudioRoutingController(BaseEngine* engine, CompletionSender* sender) : _engine(engine), _sender(sender) {}\n\n    ~AudioRoutingController() override = default;\n\n    std::vector<control::AudioConnection> get_all_input_connections() const override;\n\n    std::vector<control::AudioConnection> get_all_output_connections() const override;\n\n    std::pair<control::ControlStatus, std::vector<control::AudioConnection>> get_input_connections_for_track(int track_id) const override;\n\n    std::pair<control::ControlStatus, std::vector<control::AudioConnection>> get_output_connections_for_track(int track_id) const override;\n\n    control::ControlResponse connect_input_channel_to_track(int track_id, int track_channel, int input_channel) override;\n\n    control::ControlResponse connect_output_channel_to_track(int track_id, int track_channel, int output_channel) override;\n\n    control::ControlResponse disconnect_input(int track_id, int track_channel, int input_channel) override;\n\n    control::ControlResponse disconnect_output(int track_id, int track_channel, int output_channel) override;\n\n    control::ControlResponse disconnect_all_inputs_from_track(int track_id) override;\n\n    control::ControlResponse disconnect_all_outputs_from_track(int track_id) override;\n\nprivate:\n    BaseEngine* _engine;\n    CompletionSender* _sender;\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif // SUSHI_AUDIO_ROUTING_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/completion_sender.h",
    "content": "/*\n* Copyright 2017-2025 Elk Audio AB\n*\n* SUSHI is free software: you can redistribute it and/or modify it under the terms of\n* the GNU Affero General Public License as published by the Free Software Foundation,\n* either version 3 of the License, or (at your option) any later version.\n*\n* SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n* PURPOSE. See the GNU Affero General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License along with\n* SUSHI. If not, see http://www.gnu.org/licenses/\n*/\n\n/**\n* @brief Extraction interface for sending events with a completion callback\n* @Copyright 2017-2025 Elk Audio AB, Stockholm\n*/\n\n#ifndef SUSHI_LIBRARY_COMPLETION_SENDER_H\n#define SUSHI_LIBRARY_COMPLETION_SENDER_H\n\n#include <memory>\n\n#include \"library/event.h\"\n\nnamespace sushi::internal::engine {\n\n/* Extension to EventStatus for Controller Events */\nenum ControlEventStatus : int\n{\n    OK                    = sushi::internal::EventStatus::HANDLED_OK,\n    ERROR                 = sushi::internal::EventStatus::ERROR,\n    UNSUPPORTED_OPERATION = sushi::internal::EventStatus::EVENT_SPECIFIC,\n    NOT_FOUND,\n    OUT_OF_RANGE,\n    INVALID_ARGUMENTS\n};\n\n/**\n * @brief Interface to send events with completion notification\n */\nclass CompletionSender\n{\npublic:\n    virtual ~CompletionSender() = default;\n\n    /**\n     * @brief Sends an event and adds a completion callback\n     * @param event Event to send\n     * @return The event id\n     */\n    virtual int send_with_completion_notification([[maybe_unused]] std::unique_ptr<Event> event)\n    {\n        return 0;\n    };\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif //SUSHI_LIBRARY_COMPLETION_SENDER_H\n"
  },
  {
    "path": "src/engine/controller/controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Controller object for external control of sushi\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"controller.h\"\n\n#include \"sushi/control_notifications.h\"\n\n#include \"engine/base_engine.h\"\n#include \"controller_common.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"controller\");\n\nnamespace sushi::internal::engine {\n\ncontrol::ControlStatus to_external_status(int status)\n{\n    switch (status)\n    {\n        case EventStatus::HANDLED_OK:                   return sushi::control::ControlStatus::OK;\n        case EventStatus::ERROR:                        return sushi::control::ControlStatus::ERROR;\n        case ControlEventStatus::UNSUPPORTED_OPERATION: return sushi::control::ControlStatus::UNSUPPORTED_OPERATION;\n        case ControlEventStatus::NOT_FOUND:             return sushi::control::ControlStatus::NOT_FOUND;\n        case ControlEventStatus::OUT_OF_RANGE:          return sushi::control::ControlStatus::OUT_OF_RANGE;\n        case ControlEventStatus::INVALID_ARGUMENTS:     return sushi::control::ControlStatus::INVALID_ARGUMENTS;\n        default:                                        return sushi::control::ControlStatus::ERROR;\n    }\n}\n\nusing namespace controller_impl;\n\nController::Controller(engine::BaseEngine* engine,\n                       midi_dispatcher::MidiDispatcher* midi_dispatcher,\n                       audio_frontend::BaseAudioFrontend* audio_frontend) : control::SushiControl(&_system_controller_impl,\n                                                                                                  &_transport_controller_impl,\n                                                                                                  &_timing_controller_impl,\n                                                                                                  &_keyboard_controller_impl,\n                                                                                                  &_audio_graph_controller_impl,\n                                                                                                  &_program_controller_impl,\n                                                                                                  &_parameter_controller_impl,\n                                                                                                  &_midi_controller_impl,\n                                                                                                  &_audio_routing_controller_impl,\n                                                                                                  &_cv_gate_controller_impl,\n                                                                                                  &_osc_controller_impl,\n                                                                                                  &_session_controller_impl),\n                                                                             _system_controller_impl(engine->audio_input_channels(),\n                                                                                                     engine->audio_output_channels()),\n                                                                             _transport_controller_impl(engine),\n                                                                             _timing_controller_impl(engine),\n                                                                             _keyboard_controller_impl(engine),\n                                                                             _audio_graph_controller_impl(engine, this),\n                                                                             _program_controller_impl(engine, this),\n                                                                             _parameter_controller_impl(engine),\n                                                                             _midi_controller_impl(engine, midi_dispatcher, this),\n                                                                             _audio_routing_controller_impl(engine, this),\n                                                                             _cv_gate_controller_impl(engine, this),\n                                                                             _osc_controller_impl(engine, this),\n                                                                             _session_controller_impl(engine,\n                                                                                                      midi_dispatcher,\n                                                                                                      audio_frontend,\n                                                                                                      this)\n{\n    _event_dispatcher = engine->event_dispatcher();\n    _processors = engine->processor_container();\n\n    _event_dispatcher->subscribe_to_parameter_change_notifications(this);\n    _event_dispatcher->subscribe_to_engine_notifications(this);\n}\n\nController::~Controller()\n{\n    _event_dispatcher->unsubscribe_from_parameter_change_notifications(this);\n    _event_dispatcher->unsubscribe_from_engine_notifications(this);\n}\n\ncontrol::ControlStatus Controller::subscribe_to_notifications(control::NotificationType type,\n                                                              control::ControlListener* listener)\n{\n    switch (type)\n    {\n        case control::NotificationType::PARAMETER_CHANGE:\n            _parameter_change_listeners.push_back(listener);\n            break;\n        case control::NotificationType::PROPERTY_CHANGE:\n            _property_change_listeners.push_back(listener);\n            break;\n        case control::NotificationType::PROCESSOR_UPDATE:\n            _processor_update_listeners.push_back(listener);\n            break;\n        case control::NotificationType::TRACK_UPDATE:\n            _track_update_listeners.push_back(listener);\n            break;\n        case control::NotificationType::TRANSPORT_UPDATE:\n            _transport_update_listeners.push_back(listener);\n            break;\n        case control::NotificationType::CPU_TIMING_UPDATE:\n            _cpu_timing_update_listeners.push_back(listener);\n            break;\n        case control::NotificationType::ASYNC_COMMAND_COMPLETION:\n            _command_completion_listeners.push_back(listener);\n            break;\n        default:\n            break;\n    }\n\n    return control::ControlStatus::OK;\n}\n\nvoid Controller::completion_callback(void*arg, Event*event, int status)\n{\n    reinterpret_cast<Controller*>(arg)->_completion_callback(event, status);\n}\n\nint Controller::process(Event* event)\n{\n    if (event->is_parameter_change_notification())\n    {\n        _notify_parameter_listeners(event);\n    }\n    else if (event->is_property_change_notification())\n    {\n        _notify_property_listeners(event);\n    }\n    else if (event->is_engine_notification())\n    {\n        _handle_engine_notifications(static_cast<const EngineNotificationEvent*>(event));\n    }\n\n    return EventStatus::NOT_HANDLED;\n}\n\nvoid Controller::_handle_engine_notifications(const EngineNotificationEvent* event)\n{\n    if (event->is_audio_graph_notification())\n    {\n        _handle_audio_graph_notifications(static_cast<const AudioGraphNotificationEvent*>(event));\n\n    }\n    else if (event->is_tempo_notification())\n    {\n        auto typed_event = static_cast<const TempoNotificationEvent*>(event);\n        _notify_transport_listeners(control::TransportNotification(\n            control::TransportAction::TEMPO_CHANGED,\n                                                               typed_event->tempo(),\n                                                               typed_event->time()));\n    }\n    else if (event->is_time_sign_notification())\n    {\n        auto typed_event = static_cast<const TimeSignatureNotificationEvent*>(event);\n        _notify_transport_listeners(\n            control::TransportNotification(control::TransportAction::TIME_SIGNATURE_CHANGED,\n                                                               to_external(typed_event->time_signature()),\n                                                               typed_event->time()));\n    }\n    else if (event->is_playing_mode_notification())\n    {\n        auto typed_event = static_cast<const PlayingModeNotificationEvent*>(event);\n        _notify_transport_listeners(\n            control::TransportNotification(control::TransportAction::PLAYING_MODE_CHANGED,\n                                                               to_external(typed_event->mode()),\n                                                               typed_event->time()));\n    }\n    else if (event->is_sync_mode_notification())\n    {\n        auto typed_event = static_cast<const SyncModeNotificationEvent*>(event);\n        _notify_transport_listeners(\n            control::TransportNotification(control::TransportAction::SYNC_MODE_CHANGED,\n                                                               to_external(typed_event->mode()),\n                                                               typed_event->time()));\n    }\n    else if (event->is_timing_notification())\n    {\n        auto typed_event = static_cast<const EngineTimingNotificationEvent*>(event);\n        _notify_timing_listeners(typed_event);\n    }\n}\n\nvoid Controller::_handle_audio_graph_notifications(const AudioGraphNotificationEvent* event)\n{\n    switch (event->action())\n    {\n        case AudioGraphNotificationEvent::Action::PROCESSOR_ADDED_TO_TRACK:\n        {\n            _notify_processor_listeners(event, control::ProcessorAction::ADDED);\n            break;\n        }\n        case AudioGraphNotificationEvent::Action::PROCESSOR_REMOVED_FROM_TRACK:\n        {\n            _notify_processor_listeners(event, control::ProcessorAction::DELETED);\n            break;\n        }\n        case AudioGraphNotificationEvent::Action::TRACK_CREATED:\n        {\n            _notify_track_listeners(event, control::TrackAction::ADDED);\n            break;\n        }\n        case AudioGraphNotificationEvent::Action::TRACK_DELETED:\n        {\n            _notify_track_listeners(event, control::TrackAction::DELETED);\n            break;\n        }\n        default:\n            // External listeners are only notified once processors are added to a track\n            break;\n    }\n}\n\nvoid Controller::_notify_parameter_listeners(const Event* event) const\n{\n    auto typed_event = static_cast<const ParameterChangeNotificationEvent*>(event);\n    control::ParameterChangeNotification notification(static_cast<int>(typed_event->processor_id()),\n                                                      static_cast<int>(typed_event->parameter_id()),\n                                                      typed_event->normalized_value(),\n                                                      typed_event->domain_value(),\n                                                      typed_event->formatted_value(),\n                                                      typed_event->time());\n    for (auto& listener : _parameter_change_listeners)\n    {\n        listener->notification(&notification);\n    }\n}\n\nvoid Controller::_notify_property_listeners(const Event* event) const\n{\n    auto typed_event = static_cast<const PropertyChangeNotificationEvent*>(event);\n    control::PropertyChangeNotification notification(static_cast<int>(typed_event->processor_id()),\n                                                     static_cast<int>(typed_event->property_id()),\n                                                     typed_event->value(),\n                                                     typed_event->time());\n    for (auto& listener : _property_change_listeners)\n    {\n        listener->notification(&notification);\n    }\n}\n\nvoid Controller::_notify_track_listeners(const AudioGraphNotificationEvent* typed_event,\n                                          control::TrackAction action) const\n{\n    control::TrackNotification notification(action,\n                                        typed_event->track(),\n                                        typed_event->time());\n    for (auto& listener : _track_update_listeners)\n    {\n        listener->notification(&notification);\n    }\n}\n\nvoid Controller::_notify_transport_listeners(const control::TransportNotification& notification) const\n{\n    for (auto& listener : _transport_update_listeners)\n    {\n        listener->notification(&notification);\n    }\n}\n\nvoid Controller::_notify_processor_listeners(const AudioGraphNotificationEvent* typed_event,\n                                             control::ProcessorAction action) const\n{\n    control::ProcessorNotification notification(action,\n                                                static_cast<int>(typed_event->processor()),\n                                                static_cast<int>(typed_event->track()),\n                                                typed_event->time());\n    for (auto& listener : _processor_update_listeners)\n    {\n        listener->notification(&notification);\n    }\n}\n\nvoid Controller::_completion_callback([[maybe_unused]] Event* event, int status)\n{\n    if (status == EventStatus::HANDLED_OK)\n    {\n        ELKLOG_LOG_DEBUG(\"Event {}, handled OK\", event->id());\n    }\n    else\n    {\n        ELKLOG_LOG_WARNING(\"Event {} returned with error code: \", event->id(), status);\n    }\n    _notify_command_completion_listeners(event, to_external_status(status));\n}\n\nvoid Controller::set_osc_frontend(control_frontend::OSCFrontend* osc_frontend)\n{\n    _osc_controller_impl.set_osc_frontend(osc_frontend);\n    _session_controller_impl.set_osc_frontend(osc_frontend);\n}\n\nvoid Controller::_notify_timing_listeners(const EngineTimingNotificationEvent* event) const\n{\n    control::CpuTimings ext;\n    ext.main = to_external(event->main_timings());\n    ext.threads.reserve(event->thread_timings().size());\n\n    for (const auto& thread : event->thread_timings())\n    {\n        ext.threads.push_back(to_external(thread));\n    }\n\n    control::CpuTimingNotification notification(std::move(ext), event->time());\n\n    for (auto& listener : _cpu_timing_update_listeners)\n    {\n        listener->notification(&notification);\n    }\n}\nvoid Controller::_notify_command_completion_listeners(const Event* event, control::ControlStatus status) const\n{\n    control::CommandCompletionNotification notification(status, event->id(), event->time());\n    for (auto& listener : _command_completion_listeners)\n    {\n        listener->notification(&notification);\n    }\n}\nint Controller::send_with_completion_notification(std::unique_ptr<Event> event)\n{\n    event->set_completion_cb(completion_callback, this);\n    int event_id = event->id();\n    _event_dispatcher->post_event(std::move(event));\n    return event_id;\n}\n\n} // end namespace sushi::internal::engine\n"
  },
  {
    "path": "src/engine/controller/controller.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Controller object for external control of sushi\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"sushi/control_notifications.h\"\n#include \"sushi/control_interface.h\"\n\n#include \"completion_sender.h\"\n#include \"engine/base_event_dispatcher.h\"\n#include \"engine/transport.h\"\n#include \"library/base_performance_timer.h\"\n#include \"engine/base_processor_container.h\"\n#include \"library/event_interface.h\"\n\n#include \"system_controller.h\"\n#include \"transport_controller.h\"\n#include \"timing_controller.h\"\n#include \"keyboard_controller.h\"\n#include \"audio_graph_controller.h\"\n#include \"parameter_controller.h\"\n#include \"program_controller.h\"\n#include \"midi_controller.h\"\n#include \"audio_routing_controller.h\"\n#include \"cv_gate_controller.h\"\n#include \"osc_controller.h\"\n#include \"session_controller.h\"\n\n#ifndef SUSHI_CONTROLLER_H\n#define SUSHI_CONTROLLER_H\n\nnamespace sushi::internal {\n\nnamespace midi_dispatcher {\nclass MidiDispatcher;\n}\n\nnamespace control_frontend {\nclass OSCFrontend;\n}\n\nnamespace audio_frontend {\nclass BaseAudioFrontend;\n}\n\nnamespace engine {\n\nclass BaseEngine;\n\nclass Controller : public control::SushiControl, EventPoster, CompletionSender\n{\npublic:\n    Controller(engine::BaseEngine* engine,\n               midi_dispatcher::MidiDispatcher* midi_dispatcher,\n               audio_frontend::BaseAudioFrontend* audio_frontend);\n\n    ~Controller() override;\n\n    control::ControlStatus subscribe_to_notifications(control::NotificationType type,\n                                                      control::ControlListener* listener) override;\n\n    /* Inherited from EventPoster */\n    int process(Event* event) override;\n\n    static void completion_callback(void* arg, Event* event, int status);\n\n    void set_osc_frontend(control_frontend::OSCFrontend* osc_frontend);\n\n    int send_with_completion_notification(std::unique_ptr<Event> event) override;\n\nprivate:\n    void _completion_callback(Event* event, int status);\n\n    void _handle_engine_notifications(const EngineNotificationEvent* event);\n\n    void _handle_audio_graph_notifications(const AudioGraphNotificationEvent* event);\n\n    void _notify_processor_listeners(const AudioGraphNotificationEvent* typed_event,\n                                     control::ProcessorAction action) const;\n\n    void _notify_track_listeners(const AudioGraphNotificationEvent* typed_event,\n                                 control::TrackAction action) const;\n\n    void _notify_transport_listeners(const control::TransportNotification& notification) const;\n\n    void _notify_parameter_listeners(const Event* event) const;\n\n    void _notify_property_listeners(const Event* event) const;\n\n    void _notify_timing_listeners(const EngineTimingNotificationEvent* event) const;\n\n    void _notify_command_completion_listeners(const Event* event, control::ControlStatus status) const;\n\n    std::vector<control::ControlListener*>      _parameter_change_listeners;\n    std::vector<control::ControlListener*>      _property_change_listeners;\n    std::vector<control::ControlListener*>      _processor_update_listeners;\n    std::vector<control::ControlListener*>      _track_update_listeners;\n    std::vector<control::ControlListener*>      _transport_update_listeners;\n    std::vector<control::ControlListener*>      _cpu_timing_update_listeners;\n    std::vector<control::ControlListener*>      _command_completion_listeners;\n\n    const engine::BaseProcessorContainer*   _processors;\n\n    controller_impl::SystemController       _system_controller_impl;\n    controller_impl::TransportController    _transport_controller_impl;\n    controller_impl::TimingController       _timing_controller_impl;\n    controller_impl::KeyboardController     _keyboard_controller_impl;\n    controller_impl::AudioGraphController   _audio_graph_controller_impl;\n    controller_impl::ProgramController      _program_controller_impl;\n    controller_impl::ParameterController    _parameter_controller_impl;\n    controller_impl::MidiController         _midi_controller_impl;\n    controller_impl::AudioRoutingController _audio_routing_controller_impl;\n    controller_impl::CvGateController       _cv_gate_controller_impl;\n    controller_impl::OscController          _osc_controller_impl;\n    controller_impl::SessionController      _session_controller_impl;\n\n    dispatcher::BaseEventDispatcher*        _event_dispatcher;\n};\n\n} // end namespace engine\n} // end namespace sushi::internal\n\n#endif // SUSHI_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/controller_common.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Container for types and functions common to several sub-controllers\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_CONTROLLER_COMMON_H\n#define SUSHI_CONTROLLER_COMMON_H\n\n#include \"sushi/control_interface.h\"\n#include \"completion_sender.h\"\n#include \"library/base_performance_timer.h\"\n\nstruct CompletionData\n{\n    sushi::internal::EventCompletionCallback callback;\n    void* data;\n};\n\nnamespace sushi::internal::engine::controller_impl {\n\n/* Convenience conversion functions between external and internal\n * enums and data structs */\ninline control::PlayingMode to_external(const sushi::internal::PlayingMode mode)\n{\n    switch (mode)\n    {\n        case PlayingMode::STOPPED:      return control::PlayingMode::STOPPED;\n        case PlayingMode::PLAYING:      return control::PlayingMode::PLAYING;\n        case PlayingMode::RECORDING:    return control::PlayingMode::RECORDING;\n        default:                        return control::PlayingMode::PLAYING;\n    }\n}\n\ninline sushi::internal::PlayingMode to_internal(const control::PlayingMode mode)\n{\n    switch (mode)\n    {\n        case control::PlayingMode::STOPPED:   return sushi::internal::PlayingMode::STOPPED;\n        case control::PlayingMode::PLAYING:   return sushi::internal::PlayingMode::PLAYING;\n        case control::PlayingMode::RECORDING: return sushi::internal::PlayingMode::RECORDING;\n        default:                              return sushi::internal::PlayingMode::PLAYING;\n    }\n}\n\ninline control::SyncMode to_external(const sushi::internal::SyncMode mode)\n{\n    switch (mode)\n    {\n        case SyncMode::INTERNAL:     return control::SyncMode::INTERNAL;\n        case SyncMode::MIDI:         return control::SyncMode::MIDI;\n        case SyncMode::GATE_INPUT:   return control::SyncMode::GATE;\n        case SyncMode::ABLETON_LINK: return control::SyncMode::LINK;\n        default:                     return control::SyncMode::INTERNAL;\n    }\n}\n\ninline sushi::internal::SyncMode to_internal(const control::SyncMode mode)\n{\n    switch (mode)\n    {\n        case control::SyncMode::INTERNAL: return sushi::internal::SyncMode::INTERNAL;\n        case control::SyncMode::MIDI:     return sushi::internal::SyncMode::MIDI;\n        case control::SyncMode::GATE:     return sushi::internal::SyncMode::GATE_INPUT;\n        case control::SyncMode::LINK:     return sushi::internal::SyncMode::ABLETON_LINK;\n        default:                          return sushi::internal::SyncMode::INTERNAL;\n    }\n}\n\ninline control::Timings to_external(const sushi::internal::performance::ProcessTimings& timings)\n{\n    return {.avg = timings.avg_case,\n            .min = timings.min_case,\n            .max = timings.max_case};\n}\n\ninline control::TimeSignature to_external(sushi::TimeSignature internal)\n{\n    return {internal.numerator, internal.denominator};\n}\n\ninline sushi::TimeSignature to_internal(control::TimeSignature ext)\n{\n    return {ext.numerator, ext.denominator};\n}\n\ninline control::MidiChannel to_external_midi_channel(int channel_int)\n{\n    switch (channel_int)\n    {\n        case 0:  return sushi::control::MidiChannel::MIDI_CH_1;\n        case 1:  return sushi::control::MidiChannel::MIDI_CH_2;\n        case 2:  return sushi::control::MidiChannel::MIDI_CH_3;\n        case 3:  return sushi::control::MidiChannel::MIDI_CH_4;\n        case 4:  return sushi::control::MidiChannel::MIDI_CH_5;\n        case 5:  return sushi::control::MidiChannel::MIDI_CH_6;\n        case 6:  return sushi::control::MidiChannel::MIDI_CH_7;\n        case 7:  return sushi::control::MidiChannel::MIDI_CH_8;\n        case 8:  return sushi::control::MidiChannel::MIDI_CH_9;\n        case 9:  return sushi::control::MidiChannel::MIDI_CH_10;\n        case 10: return sushi::control::MidiChannel::MIDI_CH_11;\n        case 11: return sushi::control::MidiChannel::MIDI_CH_12;\n        case 12: return sushi::control::MidiChannel::MIDI_CH_13;\n        case 13: return sushi::control::MidiChannel::MIDI_CH_14;\n        case 14: return sushi::control::MidiChannel::MIDI_CH_15;\n        case 15: return sushi::control::MidiChannel::MIDI_CH_16;\n        case 16: return sushi::control::MidiChannel::MIDI_CH_OMNI;\n        default: return sushi::control::MidiChannel::MIDI_CH_OMNI;\n    }\n}\n\ninline int int_from_ext_midi_channel(control::MidiChannel channel)\n{\n    switch (channel)\n    {\n        case sushi::control::MidiChannel::MIDI_CH_1: return 0;\n        case sushi::control::MidiChannel::MIDI_CH_2: return 1;\n        case sushi::control::MidiChannel::MIDI_CH_3: return 2;\n        case sushi::control::MidiChannel::MIDI_CH_4: return 3;\n        case sushi::control::MidiChannel::MIDI_CH_5: return 4;\n        case sushi::control::MidiChannel::MIDI_CH_6: return 5;\n        case sushi::control::MidiChannel::MIDI_CH_7: return 6;\n        case sushi::control::MidiChannel::MIDI_CH_8: return 7;\n        case sushi::control::MidiChannel::MIDI_CH_9: return 8;\n        case sushi::control::MidiChannel::MIDI_CH_10: return 9;\n        case sushi::control::MidiChannel::MIDI_CH_11: return 10;\n        case sushi::control::MidiChannel::MIDI_CH_12: return 11;\n        case sushi::control::MidiChannel::MIDI_CH_13: return 12;\n        case sushi::control::MidiChannel::MIDI_CH_14: return 13;\n        case sushi::control::MidiChannel::MIDI_CH_15: return 14;\n        case sushi::control::MidiChannel::MIDI_CH_16: return 15;\n        case sushi::control::MidiChannel::MIDI_CH_OMNI: return 16;\n        default: return 16;\n    }\n}\n\ninline PluginType to_internal(control::PluginType type)\n{\n    switch (type)\n    {\n        case control::PluginType::INTERNAL:   return PluginType::INTERNAL;\n        case control::PluginType::VST2X:      return PluginType::VST2X;\n        case control::PluginType::VST3X:      return PluginType::VST3X;\n        case control::PluginType::LV2:        return PluginType::LV2;\n        default:                              return PluginType::INTERNAL;\n    }\n}\n\ninline control::PluginType to_external(PluginType type)\n{\n    switch (type)\n    {\n        case PluginType::INTERNAL:   return control::PluginType::INTERNAL;\n        case PluginType::VST2X:      return control::PluginType::VST2X;\n        case PluginType::VST3X:      return control::PluginType::VST3X;\n        case PluginType::LV2:        return control::PluginType::LV2;\n        default:                     return control::PluginType::INTERNAL;\n    }\n}\n\ninline control::TrackType to_external(TrackType type)\n{\n    switch (type)\n    {\n        case TrackType::REGULAR:    return control::TrackType::REGULAR;\n        case TrackType::PRE:        return control::TrackType::PRE;\n        case TrackType::POST:       return control::TrackType::POST;\n        default:                    return control::TrackType::REGULAR;\n    }\n}\n\ninline void to_internal(sushi::internal::ProcessorState* dest, const control::ProcessorState* src)\n{\n    if (src->program.has_value()) dest->set_program(src->program.value());\n    if (src->bypassed.has_value()) dest->set_bypass(src->bypassed.value());\n    for (const auto& p : src->parameters)\n    {\n        dest->add_parameter_change(p.first, p.second);\n    }\n    for (const auto& p : src->properties)\n    {\n        dest->add_property_change(p.first, p.second);\n    }\n    // Note, binary data is be moved instead of copied, don't access src->binary_data() afterwards\n    dest->set_binary_data(std::move(src->binary_data));\n}\n\ninline void to_external(control::ProcessorState* dest, sushi::internal::ProcessorState* src)\n{\n    dest->program = src->program();\n    dest->bypassed = src->bypassed();\n    dest->parameters.reserve(src->parameters().size());\n    for (const auto& param : src->parameters())\n    {\n        dest->parameters.push_back({param.first, param.second});\n    }\n    dest->properties.reserve(src->properties().size());\n    for (const auto& property : src->properties())\n    {\n        dest->properties.push_back({property.first, property.second});\n    }\n    // Note, binary data is be moved instead of copied, don't access src->binary_data() afterwards\n    dest->binary_data = std::move(src->binary_data());\n}\n\ninline ControlEventStatus map_status(EngineReturnStatus status)\n{\n    switch (status)\n    {\n        case EngineReturnStatus::OK:\n            return ControlEventStatus::OK;\n\n        case EngineReturnStatus::INVALID_TRACK:\n        case EngineReturnStatus::INVALID_PROCESSOR:\n        case EngineReturnStatus::INVALID_PARAMETER:\n            return ControlEventStatus::NOT_FOUND;\n\n        case EngineReturnStatus::INVALID_CHANNEL:\n            return ControlEventStatus::OUT_OF_RANGE;\n\n        case EngineReturnStatus::INVALID_PLUGIN_TYPE:\n        case EngineReturnStatus::INVALID_PLUGIN_UID:\n        case EngineReturnStatus::INVALID_PLUGIN:\n            return ControlEventStatus::INVALID_ARGUMENTS;\n\n        default:\n            return ControlEventStatus::ERROR;\n    }\n}\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif // SUSHI_CONTROLLER_COMMON_H\n"
  },
  {
    "path": "src/engine/controller/cv_gate_controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"cv_gate_controller.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nELK_PUSH_WARNING\nELK_DISABLE_TYPE_LIMITS\nELK_DISABLE_UNUSED_PARAMETER\n\nint CvGateController::get_cv_input_ports() const\n{\n    return 0;\n}\n\nint CvGateController::get_cv_output_ports() const\n{\n    return 0;\n}\n\nstd::vector<control::CvConnection> CvGateController::get_all_cv_input_connections() const\n{\n    return {};\n}\n\nstd::vector<control::CvConnection> CvGateController::get_all_cv_output_connections() const\n{\n    return {};\n}\n\nstd::vector<control::GateConnection> CvGateController::get_all_gate_input_connections() const\n{\n    return {};\n}\n\nstd::vector<control::GateConnection> CvGateController::get_all_gate_output_connections() const\n{\n    return {};\n}\n\nstd::pair<control::ControlStatus, std::vector<control::CvConnection>>\nCvGateController::get_cv_input_connections_for_processor(int processor_id) const\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, std::vector<control::CvConnection>()};\n}\n\nstd::pair<control::ControlStatus, std::vector<control::CvConnection>>\nCvGateController::get_cv_output_connections_for_processor(int processor_id) const\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, std::vector<control::CvConnection>()};\n}\n\nstd::pair<control::ControlStatus, std::vector<control::GateConnection>>\nCvGateController::get_gate_input_connections_for_processor(int processor_id) const\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, std::vector<control::GateConnection>()};\n}\n\nstd::pair<control::ControlStatus, std::vector<control::GateConnection>>\nCvGateController::get_gate_output_connections_for_processor(int processor_id) const\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, std::vector<control::GateConnection>()};\n}\n\ncontrol::ControlResponse CvGateController::connect_cv_input_to_parameter(int processor_id, int parameter_id, int cv_input_id)\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\ncontrol::ControlResponse CvGateController::connect_cv_output_from_parameter(int processor_id, int parameter_id, int cv_output_id)\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\ncontrol::ControlResponse CvGateController::connect_gate_input_to_processor(int processor_id, int gate_input_id, int channel, int note_no)\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\ncontrol::ControlResponse CvGateController::connect_gate_output_from_processor(int processor_id, int gate_output_id, int channel, int note_no)\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\ncontrol::ControlResponse CvGateController::disconnect_cv_input(int processor_id, int parameter_id, int cv_input_id)\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\ncontrol::ControlResponse CvGateController::disconnect_cv_output(int processor_id, int parameter_id, int cv_output_id)\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\ncontrol::ControlResponse CvGateController::disconnect_gate_input(int processor_id, int gate_input_id, int channel, int note_no)\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\ncontrol::ControlResponse CvGateController::disconnect_gate_output(int processor_id, int gate_output_id, int channel, int note_no)\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\ncontrol::ControlResponse CvGateController::disconnect_all_cv_inputs_from_processor(int processor_id)\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\ncontrol::ControlResponse CvGateController::disconnect_all_cv_outputs_from_processor(int processor_id)\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\ncontrol::ControlResponse CvGateController::disconnect_all_gate_inputs_from_processor(int processor_id)\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\ncontrol::ControlResponse CvGateController::disconnect_all_gate_outputs_from_processor(int processor_id)\n{\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\nELK_POP_WARNING\n\n} // end namespace sushi::internal::engine::controller_impl"
  },
  {
    "path": "src/engine/controller/cv_gate_controller.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_CV_GATE_CONTROLLER_H\n#define SUSHI_CV_GATE_CONTROLLER_H\n\n#include \"sushi/control_interface.h\"\n\n#include \"completion_sender.h\"\n#include \"engine/base_engine.h\"\n#include \"engine/base_event_dispatcher.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nclass CvGateController : public control::CvGateController\n{\npublic:\n    explicit CvGateController([[maybe_unused]] BaseEngine* engine, [[maybe_unused]] CompletionSender* sender) {}\n\n    ~CvGateController() override = default;\n\n    int get_cv_input_ports() const override;\n\n    int get_cv_output_ports() const override;\n\n    std::vector<control::CvConnection> get_all_cv_input_connections() const override;\n\n    std::vector<control::CvConnection> get_all_cv_output_connections() const override;\n\n    std::vector<control::GateConnection> get_all_gate_input_connections() const override;\n\n    std::vector<control::GateConnection> get_all_gate_output_connections() const override;\n\n    std::pair<control::ControlStatus, std::vector<control::CvConnection>>\n    get_cv_input_connections_for_processor(int processor_id) const override;\n\n    std::pair<control::ControlStatus, std::vector<control::CvConnection>>\n    get_cv_output_connections_for_processor(int processor_id) const override;\n\n    std::pair<control::ControlStatus, std::vector<control::GateConnection>>\n    get_gate_input_connections_for_processor(int processor_id) const override;\n\n    std::pair<control::ControlStatus, std::vector<control::GateConnection>>\n    get_gate_output_connections_for_processor(int processor_id) const override;\n\n    control::ControlResponse connect_cv_input_to_parameter(int processor_id, int parameter_id, int cv_input_id) override;\n\n    control::ControlResponse connect_cv_output_from_parameter(int processor_id, int parameter_id, int cv_output_id) override;\n\n    control::ControlResponse connect_gate_input_to_processor(int processor_id, int gate_input_id, int channel, int note_no) override;\n\n    control::ControlResponse connect_gate_output_from_processor(int processor_id, int gate_output_id, int channel, int note_no) override;\n\n    control::ControlResponse disconnect_cv_input(int processor_id, int parameter_id, int cv_input_id) override;\n\n    control::ControlResponse disconnect_cv_output(int processor_id, int parameter_id, int cv_output_id) override;\n\n    control::ControlResponse disconnect_gate_input(int processor_id, int gate_input_id, int channel, int note_no) override;\n\n    control::ControlResponse disconnect_gate_output(int processor_id, int gate_output_id, int channel, int note_no) override;\n\n    control::ControlResponse disconnect_all_cv_inputs_from_processor(int processor_id) override;\n\n    control::ControlResponse disconnect_all_cv_outputs_from_processor(int processor_id) override;\n\n    control::ControlResponse disconnect_all_gate_inputs_from_processor(int processor_id) override;\n\n    control::ControlResponse disconnect_all_gate_outputs_from_processor(int processor_id) override;\n\nprivate:\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif // SUSHI_CV_GATE_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/keyboard_controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"keyboard_controller.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"controller\");\n\nnamespace sushi::internal::engine::controller_impl {\n\nKeyboardController::KeyboardController(BaseEngine* engine) : _event_dispatcher(engine->event_dispatcher())\n{}\n\ncontrol::ControlStatus KeyboardController::send_note_on(int track_id, int channel, int note, float velocity)\n{\n    ELKLOG_LOG_DEBUG(\"send_note_on called with track {}, note {}, velocity {}\", track_id, note, velocity);\n    auto event = std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::NOTE_ON, static_cast<ObjectId>(track_id),\n                                                 channel, note, velocity, IMMEDIATE_PROCESS);\n    _event_dispatcher->post_event(std::move(event));\n    return control::ControlStatus::OK;\n}\n\ncontrol::ControlStatus KeyboardController::send_note_off(int track_id, int channel, int note, float velocity)\n{\n    ELKLOG_LOG_DEBUG(\"send_note_off called with track {}, note {}, velocity {}\", track_id, note, velocity);\n    auto event = std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::NOTE_OFF, static_cast<ObjectId>(track_id),\n                                                 channel, note, velocity, IMMEDIATE_PROCESS);\n    _event_dispatcher->post_event(std::move(event));\n    return control::ControlStatus::OK;}\n\ncontrol::ControlStatus KeyboardController::send_note_aftertouch(int track_id, int channel, int note, float value)\n{\n    ELKLOG_LOG_DEBUG(\"send_note_aftertouch called with track {}, note {}, value {}\", track_id, note, value);\n    auto event = std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::NOTE_AFTERTOUCH, static_cast<ObjectId>(track_id),\n                                                 channel, note, value, IMMEDIATE_PROCESS);\n    _event_dispatcher->post_event(std::move(event));\n    return control::ControlStatus::OK;\n}\n\ncontrol::ControlStatus KeyboardController::send_aftertouch(int track_id, int channel, float value)\n{\n    ELKLOG_LOG_DEBUG(\"send_aftertouch called with track {} and value {}\", track_id, value);\n    auto event = std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::AFTERTOUCH, static_cast<ObjectId>(track_id),\n                                                 channel, value, IMMEDIATE_PROCESS);\n    _event_dispatcher->post_event(std::move(event));\n    return control::ControlStatus::OK;\n}\n\ncontrol::ControlStatus KeyboardController::send_pitch_bend(int track_id, int channel, float value)\n{\n    ELKLOG_LOG_DEBUG(\"send_pitch_bend called with track {} and value {}\", track_id, value);\n    auto event = std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::PITCH_BEND, static_cast<ObjectId>(track_id),\n                                                 channel, value, IMMEDIATE_PROCESS);\n    _event_dispatcher->post_event(std::move(event));\n    return control::ControlStatus::OK;\n}\n\ncontrol::ControlStatus KeyboardController::send_modulation(int track_id, int channel, float value)\n{\n    ELKLOG_LOG_DEBUG(\"send_modulation called with track {} and value {}\", track_id, value);\n    auto event = std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::MODULATION, static_cast<ObjectId>(track_id),\n                                                 channel, value, IMMEDIATE_PROCESS);\n    _event_dispatcher->post_event(std::move(event));\n    return control::ControlStatus::OK;\n}\n\n} // end namespace sushi::internal::engine::controller_impl\n"
  },
  {
    "path": "src/engine/controller/keyboard_controller.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_KEYBOARD_CONTROLLER_H\n#define SUSHI_KEYBOARD_CONTROLLER_H\n\n#include \"sushi/control_interface.h\"\n\n#include \"engine/base_engine.h\"\n#include \"engine/base_event_dispatcher.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nclass KeyboardController : public control::KeyboardController\n{\npublic:\n    explicit KeyboardController(BaseEngine* engine);\n\n    ~KeyboardController() override = default;\n\n    control::ControlStatus send_note_on(int track_id, int channel, int note, float velocity) override;\n\n    control::ControlStatus send_note_off(int track_id, int channel, int note, float velocity) override;\n\n    control::ControlStatus send_note_aftertouch(int track_id, int channel, int note, float value) override;\n\n    control::ControlStatus send_aftertouch(int track_id, int channel, float value) override;\n\n    control::ControlStatus send_pitch_bend(int track_id, int channel, float value) override;\n\n    control::ControlStatus send_modulation(int track_id, int channel, float value) override;\n\nprivate:\n    dispatcher::BaseEventDispatcher* _event_dispatcher;\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif // SUSHI_KEYBOARD_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/midi_controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"midi_controller.h\"\n\n#include \"controller_common.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\ninline int map_status(midi_dispatcher::MidiDispatcherStatus status)\n{\n    switch (status)\n    {\n        case midi_dispatcher::MidiDispatcherStatus::OK:\n            return ControlEventStatus::OK;\n\n        case midi_dispatcher::MidiDispatcherStatus::INVALID_TRACK:\n        case midi_dispatcher::MidiDispatcherStatus::INVALID_PROCESSOR:\n        case midi_dispatcher::MidiDispatcherStatus::INVALID_PARAMETER:\n            return ControlEventStatus::NOT_FOUND;\n\n        case midi_dispatcher::MidiDispatcherStatus::INVAlID_CHANNEL:\n            return ControlEventStatus::OUT_OF_RANGE;\n\n        case midi_dispatcher::MidiDispatcherStatus::INVALID_MIDI_OUTPUT:\n        case midi_dispatcher::MidiDispatcherStatus::INVALID_MIDI_INPUT:\n            return ControlEventStatus::INVALID_ARGUMENTS;\n\n        default:\n            return ControlEventStatus::ERROR;\n    }\n}\n\ncontrol::MidiCCConnection populate_cc_connection(const midi_dispatcher::CCInputConnection& connection)\n{\n    control::MidiCCConnection ext_connection;\n\n    ext_connection.processor_id = connection.input_connection.target;\n    ext_connection.parameter_id = connection.input_connection.parameter;\n    ext_connection.min_range = static_cast<int>(connection.input_connection.min_range);\n    ext_connection.max_range = static_cast<int>(connection.input_connection.max_range);\n    ext_connection.relative_mode = connection.input_connection.relative;\n    ext_connection.channel = to_external_midi_channel(connection.channel);\n    ext_connection.port = connection.port;\n    ext_connection.cc_number = connection.cc;\n\n    return ext_connection;\n}\n\ncontrol::MidiPCConnection populate_pc_connection(const midi_dispatcher::PCInputConnection& connection)\n{\n    control::MidiPCConnection ext_connection;\n\n    ext_connection.processor_id = connection.processor_id;\n    ext_connection.channel = to_external_midi_channel(connection.channel);\n    ext_connection.port = connection.port;\n\n    return ext_connection;\n}\n\n\nMidiController::MidiController(BaseEngine* engine,\n                               midi_dispatcher::MidiDispatcher* midi_dispatcher,\n                               CompletionSender* sender) : _event_dispatcher(engine->event_dispatcher()),\n                                                           _midi_dispatcher(midi_dispatcher),\n                                                           _sender(sender) {}\n\nint MidiController::get_input_ports() const\n{\n    return _midi_dispatcher->get_midi_inputs();\n}\n\nint MidiController::get_output_ports() const\n{\n    return _midi_dispatcher->get_midi_outputs();\n}\n\nstd::vector<control::MidiKbdConnection> MidiController::get_all_kbd_input_connections() const\n{\n    std::vector<control::MidiKbdConnection> returns;\n\n    const auto connections = _midi_dispatcher->get_all_kb_input_connections();\n    for (auto connection : connections)\n    {\n        control::MidiKbdConnection ext_connection;\n        ext_connection.track_id = connection.input_connection.target;\n        ext_connection.port = connection.port;\n        ext_connection.channel = to_external_midi_channel(connection.channel);\n        ext_connection.raw_midi = connection.raw_midi;\n        returns.push_back(ext_connection);\n    }\n\n    return returns;\n}\n\nstd::vector<control::MidiKbdConnection> MidiController::get_all_kbd_output_connections() const\n{\n    std::vector<control::MidiKbdConnection> returns;\n\n    const auto connections = _midi_dispatcher->get_all_kb_output_connections();\n    for (auto connection : connections)\n    {\n        control::MidiKbdConnection ext_connection;\n        ext_connection.track_id = connection.track_id;\n        ext_connection.port = connection.port;\n        ext_connection.channel = to_external_midi_channel(connection.channel);\n        ext_connection.raw_midi = false;\n        returns.push_back(ext_connection);\n    }\n\n    return returns;\n}\n\nstd::vector<control::MidiCCConnection> MidiController::get_all_cc_input_connections() const\n{\n    std::vector<control::MidiCCConnection> returns;\n\n    const auto connections = _midi_dispatcher->get_all_cc_input_connections();\n    for (auto connection : connections)\n    {\n        auto ext_connection = populate_cc_connection(connection);\n        returns.push_back(ext_connection);\n    }\n\n    return returns;\n}\n\nstd::vector<control::MidiPCConnection> MidiController::get_all_pc_input_connections() const\n{\n    std::vector<control::MidiPCConnection> returns;\n\n    const auto connections = _midi_dispatcher->get_all_pc_input_connections();\n    for (auto connection : connections)\n    {\n        auto ext_connection = populate_pc_connection(connection);\n        returns.push_back(ext_connection);\n    }\n\n    return returns;\n}\n\n\nbool MidiController::get_midi_clock_output_enabled(int port) const\n{\n    return _midi_dispatcher->midi_clock_enabled(port);\n}\n\ncontrol::ControlStatus MidiController::set_midi_clock_output_enabled(bool enabled, int port)\n{\n    auto lambda = [=, this] () -> int\n    {\n        auto status = _midi_dispatcher->enable_midi_clock(enabled, port);\n        return status == midi_dispatcher::MidiDispatcherStatus::OK? EventStatus::HANDLED_OK : EventStatus::ERROR;\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    _event_dispatcher->post_event(std::move(event));\n    return control::ControlStatus::OK;\n}\n\nstd::pair<control::ControlStatus, std::vector<control::MidiCCConnection>>\nMidiController::get_cc_input_connections_for_processor(int processor_id) const\n{\n    std::pair<control::ControlStatus, std::vector<control::MidiCCConnection>> returns;\n    returns.first = control::ControlStatus::OK;\n\n    const auto connections = _midi_dispatcher->get_cc_input_connections_for_processor(processor_id);\n    for (auto connection : connections)\n    {\n        auto ext_connection = populate_cc_connection(connection);\n        returns.second.push_back(ext_connection);\n    }\n\n    return returns;\n}\n\nstd::pair<control::ControlStatus, std::vector<control::MidiPCConnection>>\nMidiController::get_pc_input_connections_for_processor(int processor_id) const\n{\n    std::pair<control::ControlStatus, std::vector<control::MidiPCConnection>> returns;\n    returns.first = control::ControlStatus::OK;\n\n    const auto connections = _midi_dispatcher->get_pc_input_connections_for_processor(processor_id);\n    for (auto connection : connections)\n    {\n        auto ext_connection = populate_pc_connection(connection);\n        returns.second.push_back(ext_connection);\n    }\n\n    return returns;\n}\n\ncontrol::ControlResponse MidiController::connect_kbd_input_to_track(int track_id,\n                                                                    control::MidiChannel channel,\n                                                                    int port,\n                                                                    bool raw_midi)\n{\n    const int int_channel = int_from_ext_midi_channel(channel);\n\n    auto lambda = [=, this] () -> int\n    {\n        midi_dispatcher::MidiDispatcherStatus status;\n        if (!raw_midi)\n        {\n            status = _midi_dispatcher->connect_kb_to_track(port, track_id, int_channel);\n        }\n        else\n        {\n            status = _midi_dispatcher->connect_raw_midi_to_track(port, track_id, int_channel);\n        }\n\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse MidiController::connect_kbd_output_from_track(int track_id,\n                                                                       control::MidiChannel channel,\n                                                                       int port)\n{\n    const int int_channel = int_from_ext_midi_channel(channel);\n\n    auto lambda = [=, this] () -> int\n    {\n        midi_dispatcher::MidiDispatcherStatus status;\n        status = _midi_dispatcher->connect_track_to_output(port, track_id, int_channel);\n\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse MidiController::connect_cc_to_parameter(int processor_id,\n                                                                 int parameter_id,\n                                                                 control::MidiChannel channel,\n                                                                 int port,\n                                                                 int cc_number,\n                                                                 float min_range,\n                                                                 float max_range,\n                                                                 bool relative_mode)\n{\n    const int int_channel = int_from_ext_midi_channel(channel);\n\n    auto lambda = [=, this] () -> int\n    {\n        auto status = _midi_dispatcher->connect_cc_to_parameter(port, // midi_input maps to port\n                                                                processor_id,\n                                                                parameter_id,\n                                                                cc_number,\n                                                                min_range,\n                                                                max_range,\n                                                                relative_mode,\n                                                                int_channel);\n\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse MidiController::connect_pc_to_processor(int processor_id,\n                                                               control::MidiChannel channel, int port)\n{\n    const int int_channel = int_from_ext_midi_channel(channel);\n\n    auto lambda = [=, this] () -> int\n    {\n        midi_dispatcher::MidiDispatcherStatus status;\n\n        status = _midi_dispatcher->connect_pc_to_processor(port, // midi_input maps to port\n                                                           processor_id,\n                                                           int_channel);\n\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse MidiController::disconnect_kbd_input(int track_id,\n                                                            control::MidiChannel channel, int port, bool raw_midi)\n{\n    const int int_channel = int_from_ext_midi_channel(channel);\n\n    auto lambda = [=, this]() -> int\n    {\n        midi_dispatcher::MidiDispatcherStatus status;\n        if (!raw_midi)\n        {\n            status = _midi_dispatcher->disconnect_kb_from_track(port, // port maps to midi_input\n                                                                track_id,\n                                                                int_channel);\n        }\n        else\n        {\n            status = _midi_dispatcher->disconnect_raw_midi_from_track(port, // port maps to midi_input\n                                                                      track_id,\n                                                                      int_channel);\n        }\n\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse MidiController::disconnect_kbd_output(int track_id, control::MidiChannel channel, int port)\n{\n    const int int_channel = int_from_ext_midi_channel(channel);\n\n    auto lambda = [=, this] () -> int\n    {\n        midi_dispatcher::MidiDispatcherStatus status;\n        status = _midi_dispatcher->disconnect_track_from_output(port, track_id, int_channel);\n\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse MidiController::disconnect_cc(int processor_id,\n                                                     control::MidiChannel channel, int port, int cc_number)\n{\n    const int int_channel = int_from_ext_midi_channel(channel);\n\n    auto lambda = [=, this] () -> int\n    {\n        const auto status = _midi_dispatcher->disconnect_cc_from_parameter(port, // port maps to midi_input\n                                                                           processor_id,\n                                                                           cc_number,\n                                                                           int_channel);\n\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse MidiController::disconnect_pc(int processor_id, control::MidiChannel channel, int port)\n{\n    const int int_channel = int_from_ext_midi_channel(channel);\n\n    auto lambda = [=, this] () -> int\n    {\n        midi_dispatcher::MidiDispatcherStatus status;\n\n        status = _midi_dispatcher->disconnect_pc_from_processor(port,\n                                                                processor_id,\n                                                                int_channel);\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse MidiController::disconnect_all_cc_from_processor(int processor_id)\n{\n    auto lambda = [=, this] () -> int\n    {\n        const auto status = _midi_dispatcher->disconnect_all_cc_from_processor(processor_id);\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse MidiController::disconnect_all_pc_from_processor(int processor_id)\n{\n    auto lambda = [=, this] () -> int\n    {\n        const auto status = _midi_dispatcher->disconnect_all_pc_from_processor(processor_id);\n        return map_status(status);\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\n} // end namespace sushi::internal::engine::controller_impl\n"
  },
  {
    "path": "src/engine/controller/midi_controller.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_MIDI_CONTROLLER_H\n#define SUSHI_MIDI_CONTROLLER_H\n\n#include \"sushi/control_interface.h\"\n\n#include \"completion_sender.h\"\n#include \"engine/base_engine.h\"\n#include \"engine/base_event_dispatcher.h\"\n#include \"engine/midi_dispatcher.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nclass MidiController : public control::MidiController\n{\npublic:\n    MidiController(BaseEngine* engine,\n                   midi_dispatcher::MidiDispatcher* midi_dispatcher,\n                   CompletionSender* sender);\n\n    ~MidiController() override = default;\n\n    int get_input_ports() const override;\n\n    int get_output_ports() const override;\n\n    std::vector<control::MidiKbdConnection> get_all_kbd_input_connections() const override;\n\n    std::vector<control::MidiKbdConnection> get_all_kbd_output_connections() const override;\n\n    std::vector<control::MidiCCConnection> get_all_cc_input_connections() const override;\n\n    std::vector<control::MidiPCConnection> get_all_pc_input_connections() const override;\n\n    bool get_midi_clock_output_enabled(int port) const override;\n\n    std::pair<control::ControlStatus, std::vector<control::MidiCCConnection>>\n    get_cc_input_connections_for_processor(int processor_id) const override;\n\n    std::pair<control::ControlStatus, std::vector<control::MidiPCConnection>>\n    get_pc_input_connections_for_processor(int processor_id) const override;\n\n    control::ControlStatus set_midi_clock_output_enabled(bool enabled, int port) override;\n\n    control::ControlResponse connect_kbd_input_to_track(int track_id,\n                                                        control::MidiChannel channel,\n                                                        int port,\n                                                        bool raw_midi) override;\n\n    control::ControlResponse connect_kbd_output_from_track(int track_id,\n                                                           control::MidiChannel channel,\n                                                           int port) override;\n\n    control::ControlResponse connect_cc_to_parameter(int processor_id,\n                                                     int parameter_id,\n                                                     control::MidiChannel channel,\n                                                     int port,\n                                                     int cc_number,\n                                                     float min_range,\n                                                     float max_range,\n                                                     bool relative_mode) override;\n\n    control::ControlResponse connect_pc_to_processor(int processor_id,\n                                                     control::MidiChannel channel,\n                                                     int port) override;\n\n    control::ControlResponse disconnect_kbd_input(int track_id,\n                                                  control::MidiChannel channel,\n                                                  int port, bool raw_midi) override;\n\n    control::ControlResponse disconnect_kbd_output(int track_id,\n                                                   control::MidiChannel channel,\n                                                   int port) override;\n\n    control::ControlResponse disconnect_cc(int processor_id,\n                                           control::MidiChannel channel,\n                                           int port,\n                                           int cc_number) override;\n\n    control::ControlResponse disconnect_pc(int processor_id, control::MidiChannel channel, int port) override;\n\n    control::ControlResponse disconnect_all_cc_from_processor(int processor_id) override;\n\n    control::ControlResponse disconnect_all_pc_from_processor(int processor_id) override;\n\nprivate:\n    dispatcher::BaseEventDispatcher* _event_dispatcher;\n    midi_dispatcher::MidiDispatcher* _midi_dispatcher;\n    CompletionSender*                _sender;\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif // SUSHI_MIDI_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/osc_controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface of sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"osc_controller.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nOscController::OscController(BaseEngine* engine, CompletionSender* sender) : _processors(engine->processor_container()),\n                                                                             _sender(sender) {}\n\nstd::string OscController::get_send_ip() const\n{\n    if (_osc_frontend)\n    {\n        return _osc_frontend->send_ip();\n    }\n    return \"\";\n}\n\nint OscController::get_send_port() const\n{\n    if (_osc_frontend)\n    {\n        return _osc_frontend->send_port();\n    }\n    return 0;\n}\n\nint OscController::get_receive_port() const\n{\n    if (_osc_frontend)\n    {\n        return _osc_frontend->receive_port();\n    }\n    return 0;\n}\n\nstd::vector<std::string> OscController::get_enabled_parameter_outputs() const\n{\n    if (_osc_frontend)\n    {\n        return _osc_frontend->get_enabled_parameter_outputs();\n    }\n    return {};\n}\n\ncontrol::ControlResponse OscController::enable_output_for_parameter(int processor_id, int parameter_id)\n{\n    if (_osc_frontend == nullptr)\n    {\n        return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n    }\n\n    auto lambda = [=, this] () -> int\n    {\n        // Here we SHOULD use name, since it is needed for building the OSC \"Address Path\".\n        // We could avoid the _processors dependency here, though not crucial, by having 4 parameters to the call.\n\n        auto processor = _processors->processor(processor_id);\n        if (processor == nullptr)\n        {\n            return ControlEventStatus::NOT_FOUND;\n        }\n\n        auto parameter_descriptor = processor->parameter_from_id(parameter_id);\n        if (parameter_descriptor == nullptr)\n        {\n            return ControlEventStatus::NOT_FOUND;\n        }\n\n        bool status = _osc_frontend->connect_from_parameter(processor->name(), parameter_descriptor->name());\n\n        if (status == false)\n        {\n            return ControlEventStatus::ERROR;\n        }\n\n        return ControlEventStatus::OK;\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse OscController::disable_output_for_parameter(int processor_id, int parameter_id)\n{\n    if (_osc_frontend == nullptr)\n    {\n        return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n    }\n\n    auto lambda = [=, this] () -> int\n    {\n        // Here we SHOULD use name, since it is needed for building the OSC \"Address Path\".\n        // We could avoid the _processors dependency here, though not crucial, by having 4 parameters to the call.\n\n        auto processor = _processors->processor(processor_id);\n        if (processor == nullptr)\n        {\n            return EventStatus::ERROR;\n        }\n\n        auto parameter_descriptor = processor->parameter_from_id(parameter_id);\n        if (parameter_descriptor == nullptr)\n        {\n            return ControlEventStatus::NOT_FOUND;\n        }\n\n        bool status = _osc_frontend->disconnect_from_parameter(processor->name(), parameter_descriptor->name());\n\n        if (status == false)\n        {\n            return ControlEventStatus::ERROR;\n        }\n\n        return ControlEventStatus::OK;\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\nvoid OscController::set_osc_frontend(control_frontend::OSCFrontend* osc_frontend)\n{\n    _osc_frontend = osc_frontend;\n}\n\ncontrol::ControlResponse OscController::enable_all_output()\n{\n    if (_osc_frontend == nullptr)\n    {\n        return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n    }\n\n    auto lambda = [=, this] () -> int\n    {\n        _osc_frontend->connect_from_all_parameters();\n        return ControlEventStatus::OK;\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::ControlResponse OscController::disable_all_output()\n{\n    if (_osc_frontend == nullptr)\n    {\n        return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n    }\n\n    auto lambda = [=, this] () -> int\n    {\n        _osc_frontend->disconnect_from_all_parameters();\n        return ControlEventStatus::OK;\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\n} // end namespace sushi::internal::engine::controller_impl"
  },
  {
    "path": "src/engine/controller/osc_controller.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_OSC_CONTROLLER_H\n#define SUSHI_OSC_CONTROLLER_H\n\n#include \"sushi/control_interface.h\"\n\n#include \"completion_sender.h\"\n#include \"control_frontends/osc_frontend.h\"\n#include \"engine/base_engine.h\"\n#include \"engine/base_processor_container.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nclass OscController : public control::OscController\n{\npublic:\n    explicit OscController(BaseEngine* engine, CompletionSender* sender);\n\n    void set_osc_frontend(control_frontend::OSCFrontend* osc_frontend);\n\n    ~OscController() override = default;\n\n    std::string get_send_ip() const override;\n\n    int get_send_port() const override;\n\n    int get_receive_port() const override;\n\n    std::vector<std::string> get_enabled_parameter_outputs() const override;\n\n    control::ControlResponse enable_output_for_parameter(int processor_id, int parameter_id) override;\n\n    control::ControlResponse disable_output_for_parameter(int processor_id, int parameter_id) override;\n\n    control::ControlResponse enable_all_output() override;\n\n    control::ControlResponse disable_all_output() override;\n\nprivate:\n    control_frontend::OSCFrontend* _osc_frontend {nullptr};\n    const engine::BaseProcessorContainer* _processors;\n    CompletionSender* _sender;\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif // SUSHI_OSC_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/parameter_controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"parameter_controller.h\"\n\n#include \"engine/base_engine.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"controller\");\n\nnamespace sushi::internal::engine::controller_impl {\n\ninline control::ParameterType to_external(const ParameterType type)\n{\n    switch (type)\n    {\n        case ParameterType::FLOAT:      return control::ParameterType::FLOAT;\n        case ParameterType::INT:        return control::ParameterType::INT;\n        case ParameterType::BOOL:       return control::ParameterType::BOOL;\n        default:                        return control::ParameterType::FLOAT;\n    }\n}\n\ninline std::vector<control::ParameterInfo> _read_parameters(const Processor* processor)\n{\n    assert(processor != nullptr);\n    std::vector<control::ParameterInfo> infos;\n    const auto& params = processor->all_parameters();\n    for (const auto& param : params)\n    {\n        if (param->type() == ParameterType::FLOAT || param->type() == ParameterType::INT || param->type() == ParameterType::BOOL)\n        {\n            control::ParameterInfo info;\n            info.id = param->id();\n            info.type = to_external(param->type());\n            info.label = param->label();\n            info.name = param->name();\n            info.unit = param->unit();\n            info.automatable = param->automatable();\n            info.min_domain_value = param->min_domain_value();\n            info.max_domain_value = param->max_domain_value();\n            infos.push_back(info);\n        }\n    }\n    return infos;\n}\n\ninline std::vector<control::PropertyInfo> _read_properties(const Processor* processor)\n{\n    assert(processor != nullptr);\n    std::vector<control::PropertyInfo> infos;\n    const auto& params = processor->all_parameters();\n    for (const auto& param : params)\n    {\n        if (param->type() == ParameterType::STRING)\n        {\n            control::PropertyInfo info;\n            info.id = param->id();\n            info.label = param->label();\n            info.name = param->name();\n            infos.push_back(info);\n        }\n    }\n    return infos;\n}\n\nParameterController::ParameterController(BaseEngine* engine) : _event_dispatcher(engine->event_dispatcher()),\n                                                               _processors(engine->processor_container())\n{}\n\nstd::pair<control::ControlStatus, std::vector<control::ParameterInfo>> ParameterController::get_processor_parameters(int processor_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_processor_parameters called with processor {}\", processor_id);\n    const auto proc = _processors->processor(processor_id);\n    if (proc)\n    {\n        return {control::ControlStatus::OK, _read_parameters(proc.get())};\n    }\n    return {control::ControlStatus::NOT_FOUND, std::vector<control::ParameterInfo>()};\n}\n\nstd::pair<control::ControlStatus, std::vector<control::ParameterInfo>> ParameterController::get_track_parameters(int track_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_track_parameters called with processor {}\", track_id);\n    const auto track = _processors->track(track_id);\n    if (track)\n    {\n        return {control::ControlStatus::OK, _read_parameters(track.get())};\n    }\n    return {control::ControlStatus::NOT_FOUND, std::vector<control::ParameterInfo>()};\n}\n\nstd::pair<control::ControlStatus, int> ParameterController::get_parameter_id(int processor_id, const std::string& parameter_name) const\n{\n    ELKLOG_LOG_DEBUG(\"get_parameter_id called with processor {} and parameter {}\", processor_id, parameter_name);\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor == nullptr)\n    {\n        return {control::ControlStatus::NOT_FOUND, 0};\n    }\n    auto descr = processor->parameter_from_name(parameter_name);\n    if (descr != nullptr)\n    {\n        return {control::ControlStatus::OK, descr->id()};\n    }\n    return {control::ControlStatus::NOT_FOUND, 0};\n}\n\nstd::pair<control::ControlStatus, control::ParameterInfo> ParameterController::get_parameter_info(int processor_id, int parameter_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_parameter_info called with processor {} and parameter {}\", processor_id, parameter_id);\n    control::ParameterInfo info;\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor != nullptr)\n    {\n        auto descr = processor->parameter_from_id(static_cast<ObjectId>(parameter_id));\n        if (descr != nullptr)\n        {\n            info.id = descr->id();\n            info.label = descr->label();\n            info.name = descr->name();\n            info.unit = descr->unit();\n            info.type = to_external(descr->type());\n            info.min_domain_value = descr->min_domain_value();\n            info.max_domain_value = descr->max_domain_value();\n            info.automatable = descr->automatable();\n\n            return {control::ControlStatus::OK, info};\n        }\n    }\n    return {control::ControlStatus::NOT_FOUND, info};\n}\n\nstd::pair<control::ControlStatus, float> ParameterController::get_parameter_value(int processor_id, int parameter_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_parameter_value called with processor {} and parameter {}\", processor_id, parameter_id);\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor != nullptr)\n    {\n        auto[status, value] = processor->parameter_value(static_cast<ObjectId>(parameter_id));\n        if (status == ProcessorReturnCode::OK)\n        {\n            return {control::ControlStatus::OK, value};\n        }\n    }\n    return {control::ControlStatus::NOT_FOUND, 0.0f};\n}\n\nstd::pair<control::ControlStatus, float> ParameterController::get_parameter_value_in_domain(int processor_id, int parameter_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_parameter_value_normalised called with processor {} and parameter {}\", processor_id, parameter_id);\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor != nullptr)\n    {\n        auto[status, value] = processor->parameter_value_in_domain(static_cast<ObjectId>(parameter_id));\n        if (status == ProcessorReturnCode::OK)\n        {\n            return {control::ControlStatus::OK, value};\n        }\n    }\n    return {control::ControlStatus::NOT_FOUND, 0.0f};\n}\n\nstd::pair<control::ControlStatus, std::string> ParameterController::get_parameter_value_as_string(int processor_id, int parameter_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_parameter_value_as_string called with processor {} and parameter {}\", processor_id, parameter_id);\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor != nullptr)\n    {\n        auto[status, value] = processor->parameter_value_formatted(static_cast<ObjectId>(parameter_id));\n        if (status == ProcessorReturnCode::OK)\n        {\n            return {control::ControlStatus::OK, value};\n        }\n    }\n    return {control::ControlStatus::NOT_FOUND, \"\"};\n}\n\nstd::pair<control::ControlStatus, std::string> ParameterController::get_property_value(int processor_id, int property_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_property_value called with processor {} and property {}\", processor_id, property_id);\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor != nullptr)\n    {\n        auto[status, value] = processor->property_value(static_cast<ObjectId>(property_id));\n        if (status == ProcessorReturnCode::OK)\n        {\n            return {control::ControlStatus::OK, value};\n        }\n    }\n    return {control::ControlStatus::NOT_FOUND, \"\"};\n}\n\ncontrol::ControlStatus ParameterController::set_parameter_value(int processor_id, int parameter_id, float value)\n{\n    float clamped_value = std::clamp<float>(value, 0.0f, 1.0f);\n    ELKLOG_LOG_DEBUG(\"set_parameter_value called with processor {}, parameter {} and value {}\", processor_id, parameter_id, clamped_value);\n    auto event = std::make_unique<ParameterChangeEvent>(ParameterChangeEvent::Subtype::FLOAT_PARAMETER_CHANGE,\n                                                        static_cast<ObjectId>(processor_id),\n                                                        static_cast<ObjectId>(parameter_id),\n                                                        clamped_value,\n                                                        IMMEDIATE_PROCESS);\n\n    _event_dispatcher->post_event(std::move(event));\n    return control::ControlStatus::OK;\n}\n\ncontrol::ControlStatus ParameterController::set_property_value(int processor_id, int property_id, const std::string& value)\n{\n    ELKLOG_LOG_DEBUG(\"set_property_value called with processor {}, property {} and value {}\", processor_id, property_id, value);\n    auto event = std::make_unique<PropertyChangeEvent>(static_cast<ObjectId>(processor_id),\n                                                       static_cast<ObjectId>(property_id),\n                                                       value,\n                                                       IMMEDIATE_PROCESS);\n    _event_dispatcher->post_event(std::move(event));\n    return control::ControlStatus::OK;\n}\n\nstd::pair<control::ControlStatus, std::vector<control::PropertyInfo>> ParameterController::get_processor_properties(int processor_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_processor_properties called with processor {}\", processor_id);\n    const auto proc = _processors->processor(processor_id);\n    if (proc)\n    {\n        return {control::ControlStatus::OK, _read_properties(proc.get())};\n    }\n    return {control::ControlStatus::NOT_FOUND, std::vector<control::PropertyInfo>()};\n}\n\nstd::pair<control::ControlStatus, std::vector<control::PropertyInfo>> ParameterController::get_track_properties(int track_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_track_properties called with processor {}\", track_id);\n    const auto track = _processors->track(track_id);\n    if (track)\n    {\n        return {control::ControlStatus::OK, _read_properties(track.get())};\n    }\n    return {control::ControlStatus::NOT_FOUND, std::vector<control::PropertyInfo>()};\n}\n\nstd::pair<control::ControlStatus, int> ParameterController::get_property_id(int processor_id, const std::string& property_name) const\n{\n    ELKLOG_LOG_DEBUG(\"get_property_id called with processor {} and property {}\", processor_id, property_name);\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor == nullptr)\n    {\n        return {control::ControlStatus::NOT_FOUND, 0};\n    }\n    auto descriptor = processor->parameter_from_name(property_name);\n    if (descriptor && descriptor->type() == ParameterType::STRING)\n    {\n        return {control::ControlStatus::OK, descriptor->id()};\n    }\n    return {control::ControlStatus::NOT_FOUND, 0};\n}\n\nstd::pair<control::ControlStatus, control::PropertyInfo> ParameterController::get_property_info(int processor_id, int property_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_property_info called with processor {} and parameter {}\", processor_id, property_id);\n    control::PropertyInfo info;\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor != nullptr)\n    {\n        auto descriptor = processor->parameter_from_id(static_cast<ObjectId>(property_id));\n        if (descriptor && descriptor->type() == ParameterType::STRING)\n        {\n            info.id = descriptor->id();\n            info.label = descriptor->label();\n            info.name = descriptor->name();\n            return {control::ControlStatus::OK, info};\n        }\n    }\n    return {control::ControlStatus::NOT_FOUND, info};\n}\n\n} // end namespace sushi::internal::engine::controller_impl\n"
  },
  {
    "path": "src/engine/controller/parameter_controller.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_PARAMETER_CONTROLLER_H\n#define SUSHI_PARAMETER_CONTROLLER_H\n\n#include \"sushi/control_interface.h\"\n\n#include \"engine/base_event_dispatcher.h\"\n#include \"engine/base_processor_container.h\"\n\nnamespace sushi::internal::engine {\n\nclass BaseEngine;\n\nnamespace controller_impl {\n\nclass ParameterController : public control::ParameterController\n{\npublic:\n    explicit ParameterController(BaseEngine* engine);\n\n    std::pair<control::ControlStatus, std::vector<control::ParameterInfo>> get_processor_parameters(int processor_id) const override;\n\n    std::pair<control::ControlStatus, std::vector<control::ParameterInfo>> get_track_parameters(int track_id) const override;\n\n    std::pair<control::ControlStatus, int> get_parameter_id(int processor_id, const std::string& parameter) const override;\n\n    std::pair<control::ControlStatus, control::ParameterInfo> get_parameter_info(int processor_id, int parameter_id) const override;\n\n    std::pair<control::ControlStatus, float> get_parameter_value(int processor_id, int parameter_id) const override;\n\n    std::pair<control::ControlStatus, float> get_parameter_value_in_domain(int processor_id, int parameter_id) const override;\n\n    std::pair<control::ControlStatus, std::string> get_parameter_value_as_string(int processor_id, int parameter_id) const override;\n\n    control::ControlStatus set_parameter_value(int processor_id, int parameter_id, float value) override;\n\n    std::pair<control::ControlStatus, std::vector<control::PropertyInfo>>  get_processor_properties(int processor_id) const override;\n\n    std::pair<control::ControlStatus, std::vector<control::PropertyInfo>>  get_track_properties(int processor_id) const override;\n\n    std::pair<control::ControlStatus, int> get_property_id(int processor_id, const std::string& property_name) const override;\n\n    std::pair<control::ControlStatus, control::PropertyInfo> get_property_info(int processor_id, int property_id) const override;\n\n    std::pair<control::ControlStatus, std::string> get_property_value(int processor_id, int parameter_id) const override;\n\n    control::ControlStatus set_property_value(int processor_id, int property_id, const std::string& value) override;\n\nprivate:\n    dispatcher::BaseEventDispatcher*        _event_dispatcher;\n    const engine::BaseProcessorContainer*   _processors;\n};\n\n} // end namespace controller_impl\n\n} // end namespace sushi::internal::engine\n\n#endif // SUSHI_PARAMETER_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/program_controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"program_controller.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"controller\");\n\nnamespace sushi::internal::engine::controller_impl {\n\nProgramController::ProgramController(BaseEngine* engine, CompletionSender* sender) : /* _engine(engine), */\n                                                                                     _processors(engine->processor_container()),\n                                                                                     _sender(sender)\n{}\n\nstd::pair<control::ControlStatus, int> ProgramController::get_processor_current_program(int processor_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_processor_current_program called with processor {}\", processor_id);\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor == nullptr)\n    {\n        return {control::ControlStatus::NOT_FOUND, 0};\n    }\n    if (processor->supports_programs())\n    {\n        return {control::ControlStatus::OK, processor->current_program()};\n    }\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, 0};\n}\n\nstd::pair<control::ControlStatus, std::string> ProgramController::get_processor_current_program_name(int processor_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_processor_current_program_name called with processor {}\", processor_id);\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor == nullptr)\n    {\n        return {control::ControlStatus::NOT_FOUND, \"\"};\n    }\n    if (processor->supports_programs())\n    {\n        return {control::ControlStatus::OK, processor->current_program_name()};\n    }\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, \"\"};\n}\n\nstd::pair<control::ControlStatus, std::string> ProgramController::get_processor_program_name(int processor_id, int program_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_processor_program_name called with processor {}\", processor_id);\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor == nullptr)\n    {\n        return {control::ControlStatus::NOT_FOUND, \"\"};\n    }\n    else if (processor->supports_programs() == false)\n    {\n        return {control::ControlStatus::UNSUPPORTED_OPERATION, \"\"};\n    }\n    auto [status, name] = processor->program_name(program_id);\n    if (status == ProcessorReturnCode::OK)\n    {\n        return {control::ControlStatus::OK, std::move(name)};\n    }\n    return {control::ControlStatus::OUT_OF_RANGE, \"\"};\n}\n\nstd::pair<control::ControlStatus, std::vector<std::string>> ProgramController::get_processor_programs(int processor_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_processor_program_name called with processor {}\", processor_id);\n    auto processor = _processors->processor(static_cast<ObjectId>(processor_id));\n    if (processor == nullptr)\n    {\n        return {control::ControlStatus::NOT_FOUND, std::vector<std::string>()};\n    }\n    else if (processor->supports_programs() == false)\n    {\n        return {control::ControlStatus::UNSUPPORTED_OPERATION, std::vector<std::string>()};\n    }\n    auto [status, names] = processor->all_program_names();\n    if (status == ProcessorReturnCode::OK)\n    {\n        return {control::ControlStatus::OK, std::move(names)};\n    }\n    return {control::ControlStatus::OUT_OF_RANGE, std::vector<std::string>()};\n}\n\ncontrol::ControlResponse ProgramController::set_processor_program(int processor_id, int program_id)\n{\n    ELKLOG_LOG_DEBUG(\"set_processor_program called with processor {} and program {}\", processor_id, program_id);\n    auto event = std::make_unique<ProgramChangeEvent>(static_cast<ObjectId>(processor_id), program_id, IMMEDIATE_PROCESS);\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\n} // end namespace sushi::internal::engine::controller_impl\n"
  },
  {
    "path": "src/engine/controller/program_controller.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_PROGRAM_CONTROLLER_H\n#define SUSHI_PROGRAM_CONTROLLER_H\n\n#include \"sushi/control_interface.h\"\n\n#include \"completion_sender.h\"\n#include \"engine/base_engine.h\"\n#include \"engine/base_event_dispatcher.h\"\n#include \"engine/base_processor_container.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nclass ProgramController : public control::ProgramController\n{\npublic:\n    explicit ProgramController(BaseEngine* engine, CompletionSender* sender);\n\n    ~ProgramController() override = default;\n\n    std::pair<control::ControlStatus, int> get_processor_current_program(int processor_id) const override;\n\n    std::pair<control::ControlStatus, std::string> get_processor_current_program_name(int processor_id) const override;\n\n    std::pair<control::ControlStatus, std::string> get_processor_program_name(int processor_id, int program_id) const override;\n\n    std::pair<control::ControlStatus, std::vector<std::string>> get_processor_programs(int processor_id) const override;\n\n    control::ControlResponse set_processor_program(int processor_id, int program_id) override;\n\nprivate:\n    const BaseProcessorContainer*       _processors;\n    CompletionSender*                   _sender;\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif // SUSHI_PROGRAM_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/real_time_controller.cpp",
    "content": "/*\n* Copyright 2017-2022 Modern Ancient Instruments Networked AB, dba Elk\n*\n* SUSHI is free software: you can redistribute it and/or modify it under the terms of\n* the GNU Affero General Public License as published by the Free Software Foundation,\n* either version 3 of the License, or (at your option) any later version.\n*\n* SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n* PURPOSE. See the GNU Affero General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License along with\n* SUSHI. If not, see http://www.gnu.org/licenses/\n*/\n\n#include \"real_time_controller.h\"\n\n#include \"audio_frontends/reactive_frontend.h\"\n#include \"control_frontends/reactive_midi_frontend.h\"\n\n#include \"engine/audio_engine.h\"\n#include \"engine/controller/controller_common.h\"\n#include \"engine/event_timer.h\"\n\nnamespace sushi::internal\n{\n\nRealTimeController::RealTimeController(audio_frontend::ReactiveFrontend* audio_frontend,\n                                       midi_frontend::ReactiveMidiFrontend* midi_frontend,\n                                       engine::Transport* transport) : _audio_frontend(audio_frontend),\n                                                                       _midi_frontend(midi_frontend),\n                                                                       _transport(transport)\n{\n}\n\nvoid RealTimeController::set_tempo(float tempo)\n{\n    if (_tempo != tempo)\n    {\n        _transport->set_tempo(tempo, false); // update_via_event\n        _tempo = tempo;\n    }\n}\n\nvoid RealTimeController::set_time_signature(control::TimeSignature time_signature)\n{\n    auto internal_time_signature = engine::controller_impl::to_internal(time_signature);\n\n    if (_time_signature != internal_time_signature)\n    {\n        _transport->set_time_signature(internal_time_signature, false); // update_via_event\n\n        _time_signature = internal_time_signature;\n    }\n}\n\nvoid RealTimeController::set_playing_mode(control::PlayingMode mode)\n{\n    auto internal_playing_mode = engine::controller_impl::to_internal(mode);\n\n    if (_playing_mode != mode)\n    {\n        _transport->set_playing_mode(internal_playing_mode,\n                                     false); // update_via_event\n        _playing_mode = mode;\n    }\n}\n\nbool RealTimeController::set_current_beats(double beat_count)\n{\n    if (_transport->position_source() == PositionSource::EXTERNAL)\n    {\n        _transport->set_current_beats(beat_count);\n        return true;\n    }\n\n    return false;\n}\n\nbool RealTimeController::set_current_bar_beats(double bar_beat_count)\n{\n    if (_transport->position_source() == PositionSource::EXTERNAL)\n    {\n        _transport->set_current_bar_beats(bar_beat_count);\n        return true;\n    }\n\n    return false;\n}\n\nvoid RealTimeController::set_position_source(TransportPositionSource ps)\n{\n    if (ps == sushi::TransportPositionSource::CALCULATED)\n    {\n        _transport->set_position_source(PositionSource::CALCULATED);\n    }\n    else\n    {\n        _transport->set_position_source(PositionSource::EXTERNAL);\n    }\n}\n\nvoid RealTimeController::process_audio(ChunkSampleBuffer& in_buffer,\n                                       ChunkSampleBuffer& out_buffer,\n                                       Time timestamp)\n{\n    _audio_frontend->process_audio(in_buffer,\n                                   out_buffer,\n                                   _samples_since_start,\n                                   timestamp);\n}\n\nvoid RealTimeController::notify_interrupted_audio(Time duration)\n{\n    _audio_frontend->notify_interrupted_audio(duration);\n}\n\nvoid RealTimeController::receive_midi(int input, MidiDataByte data, Time timestamp)\n{\n    _midi_frontend->receive_midi(input, data, timestamp);\n}\n\nvoid RealTimeController::set_midi_callback(ReactiveMidiCallback&& callback)\n{\n    _midi_frontend->set_callback(std::move(callback));\n}\n\nsushi::Time RealTimeController::calculate_timestamp_from_start(float sample_rate) const\n{\n    uint64_t micros = static_cast<uint64_t>(_samples_since_start * 1'000'000.0f / sample_rate);\n    auto timestamp = std::chrono::microseconds(micros);\n    return timestamp;\n}\n\nvoid RealTimeController::increment_samples_since_start(int64_t sample_count, Time)\n{\n    _samples_since_start += sample_count;\n}\n\n} // end namespace sushi"
  },
  {
    "path": "src/engine/controller/real_time_controller.h",
    "content": "/*\n* Copyright 2017-2022 Modern Ancient Instruments Networked AB, dba Elk\n*\n* SUSHI is free software: you can redistribute it and/or modify it under the terms of\n* the GNU Affero General Public License as published by the Free Software Foundation,\n* either version 3 of the License, or (at your option) any later version.\n*\n* SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n* PURPOSE. See the GNU Affero General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License along with\n* SUSHI. If not, see http://www.gnu.org/licenses/\n*/\n\n#ifndef REACTIVE_CONTROLLER_H\n#define REACTIVE_CONTROLLER_H\n\n#include \"sushi/rt_controller.h\"\n#include \"sushi/sushi.h\"\n#include \"engine/base_event_dispatcher.h\"\n\nnamespace sushi::internal {\n\nclass ConcreteSushi;\n\nnamespace audio_frontend\n{\nclass ReactiveFrontend;\n}\n\nnamespace midi_frontend\n{\nclass ReactiveMidiFrontend;\n}\n\nnamespace engine\n{\nclass Transport;\n}\n\nclass RtControllerAccessor;\n\n/**\n * @brief When a host application embeds Sushi, it should use this class to interface with Sushi in a real-time context.\n *        RealTimeController implements the RtController API.\n */\nclass RealTimeController : public RtController\n{\npublic:\n    RealTimeController(audio_frontend::ReactiveFrontend* audio_frontend,\n                       midi_frontend::ReactiveMidiFrontend* midi_frontend,\n                       engine::Transport* transport);\n\n    ~RealTimeController() override = default;\n\n    /// For Transport:\n    /////////////////////////////////////////////////////////////\n\n    void set_tempo(float tempo) override;\n\n    void set_time_signature(control::TimeSignature time_signature) override;\n\n    void set_playing_mode(control::PlayingMode mode) override;\n\n    bool set_current_beats(double beat_count) override;\n\n    bool set_current_bar_beats(double bar_beat_count) override;\n\n    void set_position_source(TransportPositionSource ps) override;\n\n    /// For Audio:\n    /////////////////////////////////////////////////////////////\n\n    void process_audio(ChunkSampleBuffer& in_buffer,\n                       ChunkSampleBuffer& out_buffer,\n                       Time timestamp) override;\n\n    void notify_interrupted_audio(sushi::Time duration) override;\n\n    /// For MIDI:\n    /////////////////////////////////////////////////////////////\n\n    void receive_midi(int input, MidiDataByte data, Time timestamp) override;\n    void set_midi_callback(ReactiveMidiCallback&& callback) override;\n\n    [[nodiscard]] sushi::Time calculate_timestamp_from_start(float sample_rate) const override;\n    void increment_samples_since_start(int64_t sample_count, Time timestamp) override;\n\nprivate:\n    friend RtControllerAccessor;\n\n    audio_frontend::ReactiveFrontend* _audio_frontend {nullptr};\n    midi_frontend::ReactiveMidiFrontend* _midi_frontend {nullptr};\n    engine::Transport* _transport {nullptr};\n    int64_t _samples_since_start {0};\n\n    float _tempo {0};\n    sushi::TimeSignature _time_signature {0, 0};\n    control::PlayingMode _playing_mode {control::PlayingMode::STOPPED};\n};\n\n} // end namespace sushi::internal\n\n#endif // REACTIVE_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/session_controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <ctime>\n\n#include \"spdlog/fmt/bundled/format.h\"\n#include \"spdlog/fmt/bundled/chrono.h\"\n\n#include \"elklog/static_logger.h\"\n\n#include \"sushi/constants.h\"\n#include \"sushi/compile_time_settings.h\"\n\n#include \"session_controller.h\"\n#include \"controller_common.h\"\n\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"controller\");\n\nnamespace sushi::internal::engine::controller_impl {\n\ninline control::TrackAudioConnectionState to_external(const AudioConnection& con,\n                                                      const std::string& track_name)\n{\n    control::TrackAudioConnectionState ext_con;\n    ext_con.track = track_name;\n    ext_con.track_channel = con.track_channel;\n    ext_con.engine_channel = con.engine_channel;\n    return ext_con;\n}\n\ninline control::MidiKbdConnectionState to_external(const midi_dispatcher::KbdInputConnection& con,\n                                                   const std::string& track_name)\n{\n    control::MidiKbdConnectionState ext_con;\n    ext_con.track = track_name;\n    ext_con.channel = to_external_midi_channel(con.channel);\n    ext_con.port = con.port;\n    ext_con.raw_midi = con.raw_midi;\n    return ext_con;\n}\n\ninline control::MidiKbdConnectionState to_external(const midi_dispatcher::KbdOutputConnection& con,\n                                                   const std::string& track_name)\n{\n    control::MidiKbdConnectionState ext_con;\n    ext_con.track = track_name;\n    ext_con.channel = to_external_midi_channel(con.channel);\n    ext_con.port = con.port;\n    ext_con.raw_midi = false;\n    return ext_con;\n}\n\ninline control::MidiCCConnectionState to_external(const midi_dispatcher::CCInputConnection& con,\n                                                  const std::string& track_name)\n{\n    control::MidiCCConnectionState ext_con;\n    ext_con.processor = track_name;\n    ext_con.channel = to_external_midi_channel(con.channel);\n    ext_con.port = con.port;\n    ext_con.parameter_id = con.input_connection.parameter;\n    ext_con.cc_number = con.cc;\n    ext_con.min_range = con.input_connection.min_range;\n    ext_con.max_range = con.input_connection.max_range;\n    ext_con.relative_mode = con.input_connection.relative;\n    return ext_con;\n}\n\ninline control::MidiPCConnectionState to_external(const midi_dispatcher::PCInputConnection& con,\n                                                  const std::string& track_name)\n{\n    control::MidiPCConnectionState ext_con;\n    ext_con.processor = track_name;\n    ext_con.channel = to_external_midi_channel(con.channel);\n    ext_con.port = con.port;\n    return ext_con;\n}\n\ninline void to_internal(control_frontend::OscState& dest, const control::OscState& src)\n{\n    dest.set_auto_enable_outputs(src.enable_all_processor_outputs);\n    for (const auto& output : src.enabled_processor_outputs)\n    {\n        dest.add_enabled_outputs(std::string(output.processor), {output.parameter_ids.begin(), output.parameter_ids.end()});\n    }\n}\n\ninline void to_external(control::OscState& dest, const control_frontend::OscState& src)\n{\n    dest.enable_all_processor_outputs = src.auto_enable_outputs();\n    for (const auto& output : src.enabled_outputs())\n    {\n        dest.enabled_processor_outputs.push_back({output.first, {output.second.begin(), output.second.end()}});\n    }\n}\n\nSessionController::SessionController(BaseEngine* engine,\n                                     midi_dispatcher::MidiDispatcher* midi_dispatcher,\n                                     audio_frontend::BaseAudioFrontend* audio_frontend,\n                                     CompletionSender* sender) : _sender(sender),\n                                                                 _engine(engine),\n                                                                 _midi_dispatcher(midi_dispatcher),\n                                                                 _audio_frontend(audio_frontend),\n                                                                 _processors(engine->processor_container()),\n                                                                 _osc_frontend(nullptr)\n{}\n\nvoid SessionController::set_osc_frontend(control_frontend::OSCFrontend* osc_frontend)\n{\n    _osc_frontend = osc_frontend;\n}\n\ncontrol::SessionState SessionController::save_session() const\n{\n    ELKLOG_LOG_DEBUG(\"save_session called\");\n\n    control::SessionState session;\n    auto date = time(nullptr);\n    session.save_date = fmt::format(\"{:%Y-%m-%d %H:%M}\", fmt::localtime(date));\n    session.sushi_info = _save_build_info();\n    session.osc_state = _save_osc_state();\n    session.midi_state = _save_midi_state();\n    session.engine_state = _save_engine_state();\n    session.tracks = _save_tracks();\n\n    return session;\n}\n\ncontrol::ControlResponse SessionController::restore_session(const control::SessionState& state)\n{\n    ELKLOG_LOG_DEBUG(\"restore_session called\");\n    if (_check_state(state) == false)\n    {\n        return {control::ControlStatus::INVALID_ARGUMENTS, 0};\n    }\n\n    auto new_session = std::make_unique<control::SessionState>(state);\n\n    auto lambda = [&, state = std::move(new_session)] () -> int\n    {\n        bool realtime = _engine->realtime();\n        if (realtime)\n        {\n            ELKLOG_LOG_DEBUG(\"Pausing engine\");\n            _audio_frontend->pause(true);\n        }\n\n        _clear_all_tracks();\n        _restore_tracks(state->tracks);\n        _restore_plugin_states(state->tracks);\n        _restore_engine(state->engine_state);\n        _restore_midi(state->midi_state);\n        _restore_osc(state->osc_state);\n\n        if (realtime)\n        {\n            ELKLOG_LOG_DEBUG(\"Un-Pausing engine\");\n            _audio_frontend->pause(false);\n        }\n\n        return EventStatus::HANDLED_OK;\n    };\n\n    std::unique_ptr<Event> event(new LambdaEvent(std::move(lambda), IMMEDIATE_PROCESS));\n    return {control::ControlStatus::ASYNC_RESPONSE, _sender->send_with_completion_notification(std::move(event))};\n}\n\ncontrol::SushiBuildInfo SessionController::_save_build_info() const\n{\n    control::SushiBuildInfo info;\n    for(auto& option : sushi::CompileTimeSettings::enabled_build_options)\n    {\n        info.build_options.emplace_back(option);\n    }\n\n    info.version = CompileTimeSettings::sushi_version;\n    info.audio_buffer_size = AUDIO_CHUNK_SIZE;\n    info.commit_hash = SUSHI_GIT_COMMIT_HASH;\n    info.build_date = SUSHI_BUILD_TIMESTAMP;\n    return info;\n}\n\ncontrol::OscState SessionController::_save_osc_state() const\n{\n    control::OscState ext_state;\n    if (_osc_frontend)\n    {\n        auto state = _osc_frontend->save_state();\n        to_external(ext_state, state);\n    }\n    return ext_state;\n}\n\ncontrol::MidiState SessionController::_save_midi_state() const\n{\n    control::MidiState state;\n\n    state.inputs = _midi_dispatcher->get_midi_inputs();\n    state.outputs = _midi_dispatcher->get_midi_outputs();\n\n    for (const auto& con : _midi_dispatcher->get_all_kb_input_connections())\n    {\n        auto track = _processors->track(con.input_connection.target);\n        if (track)\n        {\n            state.kbd_input_connections.push_back(to_external(con, track->name()));\n        }\n    }\n    for (const auto& con : _midi_dispatcher->get_all_kb_output_connections())\n    {\n        auto track = _processors->track(con.track_id);\n        if (track)\n        {\n            state.kbd_output_connections.push_back(to_external(con, track->name()));\n        }\n    }\n    for (const auto& con : _midi_dispatcher->get_all_cc_input_connections())\n    {\n        auto processor = _processors->processor(con.input_connection.target);\n        if (processor)\n        {\n            state.cc_connections.push_back(to_external(con, processor->name()));\n        }\n    }\n    for (const auto& con : _midi_dispatcher->get_all_pc_input_connections())\n    {\n        auto processor = _processors->processor(con.processor_id);\n        if (processor)\n        {\n            state.pc_connections.push_back(to_external(con, processor->name()));\n        }\n    }\n    for (int port = 0; port < _midi_dispatcher->get_midi_outputs(); ++port)\n    {\n        if (_midi_dispatcher->midi_clock_enabled(port))\n        {\n            state.enabled_clock_outputs.push_back(port);\n        }\n    }\n    return state;\n}\n\ncontrol::EngineState SessionController::_save_engine_state() const\n{\n    control::EngineState state;\n    auto transport = _engine->transport();\n\n    state.sample_rate = _engine->sample_rate();\n    state.tempo = transport->current_tempo();\n    state.playing_mode = to_external(transport->playing_mode());\n    state.sync_mode = to_external(transport->sync_mode());\n    state.time_signature = to_external(transport->time_signature());\n    state.input_clip_detection = _engine->input_clip_detection();\n    state.output_clip_detection = _engine->output_clip_detection();\n    state.master_limiter = _engine->master_limiter();\n    state.used_audio_inputs = _engine->audio_input_channels();\n    state.used_audio_outputs = _engine->audio_output_channels();\n\n    int audio_inputs = 0;\n    for (const auto& con : _engine->audio_input_connections())\n    {\n        auto track = _processors->track(con.track);\n        if (track)\n        {\n            state.input_connections.push_back(to_external(con, track->name()));\n            audio_inputs = std::max(audio_inputs, con.engine_channel);\n        }\n    }\n    // Store the minimum number of audio channels required to restore the session\n    state.used_audio_inputs = audio_inputs;\n\n    int audio_outputs = 0;\n    for (const auto& con : _engine->audio_output_connections())\n    {\n        auto track = _processors->track(con.track);\n        if (track)\n        {\n            state.output_connections.push_back(to_external(con, track->name()));\n            audio_outputs = std::max(audio_outputs, con.engine_channel);\n        }\n    }\n    state.used_audio_outputs = audio_outputs;\n\n    return state;\n}\n\nstd::vector<control::TrackState> SessionController::_save_tracks() const\n{\n    std::vector<control::TrackState> tracks;\n    for (const auto& track : _processors->all_tracks())\n    {\n        control::TrackState state;\n        auto track_state = track->save_state();\n        to_external(&state.track_state, &track_state);\n        state.name = track->name();\n        state.label = track->label();\n        state.channels = track->input_channels();\n        state.buses = track->buses();\n        state.type = to_external(track->type());\n        state.thread = track->thread();\n\n        for (const auto& plugin : _processors->processors_on_track(track->id()))\n        {\n            state.processors.push_back(_save_plugin(plugin.get()));\n        }\n\n        tracks.push_back(std::move(state));\n    }\n    return tracks;\n}\n\ncontrol::PluginClass SessionController::_save_plugin(const Processor* plugin) const\n{\n    control::PluginClass plugin_class;\n\n    auto info = plugin->info();\n    plugin_class.name = plugin->name();\n    plugin_class.label = plugin->label();\n    plugin_class.uid = info.uid;\n    plugin_class.path = info.path;\n    plugin_class.type = to_external(info.type);\n\n    auto plugin_state = plugin->save_state();\n    to_external(&plugin_class.state, &plugin_state);\n\n    return plugin_class;\n}\n\nbool SessionController::_check_state(const control::SessionState& state) const\n{\n    // Todo: check if state was saved with a newer/older version of sushi.\n    if (state.engine_state.used_audio_inputs > _engine->audio_input_channels() ||\n        state.engine_state.used_audio_outputs > _engine->audio_output_channels())\n    {\n        ELKLOG_LOG_ERROR(\"Audio engine doesn't have enough audio channels to restore saved session\");\n        return false;\n    }\n\n    if (state.midi_state.inputs > _midi_dispatcher->get_midi_inputs() ||\n        state.midi_state.outputs > _midi_dispatcher->get_midi_outputs())\n    {\n        ELKLOG_LOG_ERROR(\"Not enough midi inputs or outputs to restore saved session\");\n        return false;\n    }\n    return true;\n}\n\nvoid SessionController::_restore_tracks(std::vector<control::TrackState> tracks)\n{\n    for (auto& track : tracks)\n    {\n        EngineReturnStatus status;\n        ObjectId track_id;\n\n        switch (track.type)\n        {\n            case control::TrackType::PRE:\n                std::tie(status, track_id) = _engine->create_pre_track(track.name);\n                break;\n\n            case control::TrackType::POST:\n                std::tie(status, track_id) = _engine->create_post_track(track.name);\n                break;\n\n            case control::TrackType::REGULAR:\n                if (track.buses > 1)\n                {\n                    std::tie(status, track_id) = _engine->create_multibus_track(track.name, track.buses, track.thread);\n                }\n                else\n                {\n                    std::tie(status, track_id) = _engine->create_track(track.name, track.channels, track.thread);\n                }\n                break;\n\n            default:\n                track_id = 0;\n                status = EngineReturnStatus::INVALID_TRACK;\n        }\n\n        auto track_instance = _processors->mutable_track(track_id);\n\n        if (status != EngineReturnStatus::OK || !track_instance)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to restore track {} with error {}\", track.name, static_cast<int>(status));\n            continue;\n        }\n\n        for (auto& plugin : track.processors)\n        {\n            _restore_plugin(plugin, track_instance.get());\n        }\n    }\n}\n\nvoid SessionController::_restore_plugin_states(std::vector<control::TrackState> tracks)\n{\n    for (auto& track : tracks)\n    {\n        auto track_instance = _processors->mutable_track(track.name);\n        if (!track_instance)\n        {\n            ELKLOG_LOG_ERROR(\"Track {} not found\", track.name);\n            continue;\n        }\n\n        ProcessorState state;\n        to_internal(&state, &track.track_state);\n        auto status = track_instance->set_state(&state, false);\n        if (status != ProcessorReturnCode::OK)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to restore state to track {} with status {}\", track.name, static_cast<int>(status));\n        }\n\n        for (auto& plugin : track.processors)\n        {\n            auto instance = _processors->mutable_processor(plugin.name);\n            if (!instance)\n            {\n                ELKLOG_LOG_ERROR(\"Plugin {} not found\", plugin.name);\n                continue;\n            }\n            to_internal(&state, &plugin.state);\n            status = instance->set_state(&state, false);\n            if (status != ProcessorReturnCode::OK)\n            {\n                ELKLOG_LOG_ERROR(\"Failed to restore state to track {} with status {}\", track.name, static_cast<int>(status));\n            }\n        }\n    }\n}\n\nvoid SessionController::_restore_plugin(control::PluginClass plugin, Track* track)\n{\n    PluginInfo info {.uid = plugin.uid,\n                     .path = plugin.path,\n                     .type = to_internal(plugin.type)};\n    auto [status, processor_id] = _engine->create_processor(info, plugin.name);\n    auto instance = _processors->mutable_processor(processor_id);\n\n    if (status == EngineReturnStatus::OK && instance)\n    {\n        instance->set_label(plugin.label);\n        _engine->add_plugin_to_track(instance->id(), track->id());\n    }\n    else\n    {\n        ELKLOG_LOG_ERROR(\"Failed to restore plugin {} on track {}\", plugin.name, track->name());\n    }\n}\n\nvoid SessionController::_restore_engine(control::EngineState& state)\n{\n    if (_engine->sample_rate() != state.sample_rate)\n    {\n        ELKLOG_LOG_WARNING(\"Saved session samplerate mismatch({}Hz vs {}Hz\", _engine->sample_rate(), state.sample_rate);\n    }\n    _engine->set_tempo(state.tempo);\n    _engine->set_tempo_sync_mode(to_internal(state.sync_mode));\n    _engine->set_transport_mode(to_internal(state.playing_mode));\n    _engine->set_time_signature(to_internal(state.time_signature));\n    _engine->enable_input_clip_detection(state.input_clip_detection);\n    _engine->enable_output_clip_detection(state.output_clip_detection);\n    _engine->enable_master_limiter(state.master_limiter);\n\n    [[maybe_unused]] EngineReturnStatus status;\n\n    for (const auto& con : state.input_connections)\n    {\n        auto track = _processors->track(con.track);\n        if (track)\n        {\n            status = _engine->connect_audio_input_channel(con.engine_channel, con.track_channel, track->id());\n            ELKLOG_LOG_ERROR_IF(status != EngineReturnStatus::OK, \"Failed to connect channel {} of track {} to engine channel {}\",\n                               con.track_channel, con.engine_channel, con.track);\n        }\n    }\n\n    for (const auto& con : state.output_connections)\n    {\n        auto track = _processors->track(con.track);\n        if (track)\n        {\n            status = _engine->connect_audio_output_channel(con.engine_channel, con.track_channel, track->id());\n            ELKLOG_LOG_ERROR_IF(status != EngineReturnStatus::OK, \"Failed to connect engine channel {} from channel {} of track\",\n                               con.engine_channel, con.track_channel, con.track);\n        }\n    }\n}\n\nvoid SessionController::_restore_midi(control::MidiState& state)\n{\n    [[maybe_unused]] midi_dispatcher::MidiDispatcherStatus status;\n    for (const auto& con : state.kbd_input_connections)\n    {\n        auto track = _processors->track(con.track);\n        if (track)\n        {\n            if (con.raw_midi)\n            {\n                status = _midi_dispatcher->connect_raw_midi_to_track(con.port, track->id(), int_from_ext_midi_channel(con.channel));\n            }\n            else\n            {\n                status = _midi_dispatcher->connect_kb_to_track(con.port, track->id(), int_from_ext_midi_channel(con.channel));\n            }\n            ELKLOG_LOG_ERROR_IF(status != midi_dispatcher::MidiDispatcherStatus::OK,\n                               \"Failed to connect midi kbd to track {}\", track->name());\n        }\n    }\n\n    for (const auto& con : state.kbd_output_connections)\n    {\n        auto track = _processors->track(con.track);\n        if (track)\n        {\n            status = _midi_dispatcher->connect_track_to_output(con.port, track->id(), int_from_ext_midi_channel(con.channel));\n            ELKLOG_LOG_ERROR_IF(status != midi_dispatcher::MidiDispatcherStatus::OK,\n                               \"Failed to connect midi kbd from track {} to output\", track->name());\n        }\n    }\n\n    for (const auto& con : state.cc_connections)\n    {\n        auto processor = _processors->processor(con.processor);\n        if (processor)\n        {\n            status = _midi_dispatcher->connect_cc_to_parameter(con.port, processor->id(), con.parameter_id,\n                                                               con.cc_number, con.min_range, con.max_range,\n                                                               con.relative_mode, int_from_ext_midi_channel(con.channel));\n            ELKLOG_LOG_ERROR_IF(status != midi_dispatcher::MidiDispatcherStatus::OK,\n                               \"Failed to connect midi cc to parameter {} of processor {}\", con.parameter_id, processor->id());\n        }\n    }\n\n    for (const auto& con : state.pc_connections)\n    {\n        auto processor = _processors->processor(con.processor);\n        if (processor)\n        {\n            status = _midi_dispatcher->connect_pc_to_processor(con.port, processor->id(), int_from_ext_midi_channel(con.channel));\n            ELKLOG_LOG_ERROR_IF(status != midi_dispatcher::MidiDispatcherStatus::OK,\n                               \"Failed to connect mid program change to processor {}\", processor->name());\n        }\n    }\n\n    for (int port = 0; port < _midi_dispatcher->get_midi_outputs(); ++port)\n    {\n        _midi_dispatcher->enable_midi_clock(false, port);\n    }\n    for (auto port : state.enabled_clock_outputs)\n    {\n        status = _midi_dispatcher->enable_midi_clock(true, port);\n    }\n}\n\nvoid SessionController::_restore_osc(control::OscState& state)\n{\n    if (_osc_frontend)\n    {\n        control_frontend::OscState internal_state;\n        to_internal(internal_state, state);\n        _osc_frontend->set_state(internal_state);\n    }\n}\n\nvoid SessionController::_clear_all_tracks()\n{\n    auto tracks = _processors->all_tracks();\n    for (const auto& track : tracks)\n    {\n        auto processors = _processors->processors_on_track(track->id());\n        for (const auto& processor : processors)\n        {\n            _engine->remove_plugin_from_track(processor->id(), track->id());\n            _engine->delete_plugin(processor->id());\n        }\n        _engine->delete_track(track->id());\n    }\n}\n\n} // end namespace sushi::engine::controller_impl\n"
  },
  {
    "path": "src/engine/controller/session_controller.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SESSION_CONTROLLER_H\n#define SUSHI_SESSION_CONTROLLER_H\n\n#include \"sushi/control_interface.h\"\n\n#include \"completion_sender.h\"\n#include \"engine/base_engine.h\"\n#include \"engine/base_event_dispatcher.h\"\n#include \"engine/midi_dispatcher.h\"\n#include \"control_frontends/osc_frontend.h\"\n#include \"audio_frontends/base_audio_frontend.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nclass Accessor;\n\nclass SessionController : public control::SessionController\n{\npublic:\n    SessionController(BaseEngine* engine,\n                      midi_dispatcher::MidiDispatcher* midi_dispatcher,\n                      audio_frontend::BaseAudioFrontend* audio_frontend,\n                      CompletionSender* sender);\n\n    ~SessionController() override = default;\n\n    void set_osc_frontend(control_frontend::OSCFrontend* osc_frontend);\n\n    [[nodiscard]] control::SessionState save_session() const override;\n\n    control::ControlResponse restore_session(const control::SessionState& state) override;\n\nprivate:\n    friend Accessor;\n\n    [[nodiscard]] control::SushiBuildInfo _save_build_info() const;\n    [[nodiscard]] control::OscState       _save_osc_state() const;\n    [[nodiscard]] control::MidiState      _save_midi_state() const;\n    [[nodiscard]] control::EngineState    _save_engine_state() const;\n    [[nodiscard]] std::vector<control::TrackState> _save_tracks() const;\n    [[nodiscard]] control::PluginClass    _save_plugin(const sushi::internal::Processor* plugin) const;\n\n    [[nodiscard]] bool _check_state(const control::SessionState& state) const;\n\n    void _restore_tracks(std::vector<control::TrackState> tracks);\n    void _restore_plugin_states(std::vector<control::TrackState> tracks);\n    void _restore_plugin(control::PluginClass plugin, sushi::internal::engine::Track* track);\n    void _restore_engine(control::EngineState& state);\n    void _restore_midi(control::MidiState& state);\n    void _restore_osc(control::OscState& state);\n    void _clear_all_tracks();\n\n    CompletionSender*                   _sender;\n    engine::BaseEngine*                 _engine;\n    midi_dispatcher::MidiDispatcher*    _midi_dispatcher;\n    audio_frontend::BaseAudioFrontend*  _audio_frontend;\n    const BaseProcessorContainer*       _processors;\n    control_frontend::OSCFrontend*      _osc_frontend;\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif // SUSHI_SESSION_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/system_controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"system_controller.h\"\n\n#include \"sushi/constants.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nSystemController::SystemController(int inputs, int outputs) : _audio_inputs{inputs}, _audio_outputs{outputs}\n{\n    for(auto& option : CompileTimeSettings::enabled_build_options)\n    {\n        _build_options.emplace_back(option);\n    }\n\n    _build_info.version = CompileTimeSettings::sushi_version;\n    _build_info.build_options = _build_options;\n    _build_info.audio_buffer_size = AUDIO_CHUNK_SIZE;\n    _build_info.commit_hash = SUSHI_GIT_COMMIT_HASH;\n    _build_info.build_date = SUSHI_BUILD_TIMESTAMP;\n}\n\nstd::string SystemController::get_sushi_version() const\n{\n    return {CompileTimeSettings::sushi_version};\n}\n\nstd::string SystemController::get_sushi_api_version() const\n{\n    return {CompileTimeSettings::sushi_api_version};\n}\n\ncontrol::SushiBuildInfo SystemController::get_sushi_build_info() const\n{\n    return _build_info;\n}\n\nint SystemController::get_input_audio_channel_count() const\n{\n    return _audio_inputs;\n}\n\nint SystemController::get_output_audio_channel_count() const\n{\n    return _audio_outputs;\n}\n\n} // end namespace sushi::internal::engine::controller_impl"
  },
  {
    "path": "src/engine/controller/system_controller.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SYSTEM_CONTROLLER_H\n#define SUSHI_SYSTEM_CONTROLLER_H\n\n#include \"sushi/compile_time_settings.h\"\n#include \"sushi/control_interface.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nclass SystemController : public control::SystemController\n{\npublic:\n    SystemController(int inputs, int outputs);\n\n    ~SystemController() override = default;\n\n    std::string get_sushi_version() const override;\n\n    std::string get_sushi_api_version() const override;\n\n    control::SushiBuildInfo get_sushi_build_info() const override;\n\n    int get_input_audio_channel_count() const override;\n\n    int get_output_audio_channel_count() const override;\n\nprivate:\n    std::vector<std::string> _build_options;\n    control::SushiBuildInfo _build_info;\n\n    const int _audio_inputs{0};\n    const int _audio_outputs{0};\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif // SUSHI_SYSTEM_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/timing_controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"timing_controller.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"controller\");\n\nnamespace sushi::internal::engine::controller_impl {\n\nTimingController::TimingController(engine::BaseEngine* engine) : _performance_timer(engine->performance_timer())\n{}\n\ninline control::Timings to_external(performance::ProcessTimings& internal)\n{\n    return {internal.avg_case, internal.min_case, internal.max_case};\n}\n\nbool TimingController::get_timing_statistics_enabled() const\n{\n    ELKLOG_LOG_DEBUG(\"get_timing_statistics_enabled called\");\n    return _performance_timer->enabled();\n}\n\nvoid TimingController::set_timing_statistics_enabled(bool enabled)\n{\n    ELKLOG_LOG_DEBUG(\"set_timing_statistics_enabled called with {}\", enabled);\n    // TODO - do this by events instead.\n    _performance_timer->enable(enabled);\n}\n\nstd::pair<control::ControlStatus, control::CpuTimings> TimingController::get_engine_timings() const\n{\n    ELKLOG_LOG_DEBUG(\"get_engine_timings called, returning \");\n    control::CpuTimings timings;\n    timings.main = _get_timings(engine::ENGINE_TIMING_ID).second;\n    int thread_id = ENGINE_TIMING_ID - 1;\n    auto [status, thread_timings] = _get_timings(thread_id);\n    while(status == control::ControlStatus::OK)\n    {\n        timings.threads.push_back(thread_timings);\n        std::tie(status, thread_timings) = _get_timings(--thread_id);\n    }\n    return {control::ControlStatus::OK, timings};\n}\n\nstd::pair<control::ControlStatus, control::Timings> TimingController::get_track_timings(int track_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_track_timings called, returning \");\n    return _get_timings(track_id);\n}\n\nstd::pair<control::ControlStatus, control::Timings> TimingController::get_processor_timings(int processor_id) const\n{\n    ELKLOG_LOG_DEBUG(\"get_processor_timings called, returning \");\n    return _get_timings(processor_id);\n}\n\ncontrol::ControlStatus TimingController::reset_all_timings()\n{\n    ELKLOG_LOG_DEBUG(\"reset_all_timings called, returning \");\n    _performance_timer->clear_all_timings();\n    return control::ControlStatus::OK;\n}\n\ncontrol::ControlStatus TimingController::reset_track_timings(int track_id)\n{\n    ELKLOG_LOG_DEBUG(\"reset_track_timings called, returning \");\n    auto success =_performance_timer->clear_timings_for_node(track_id);\n    return success? control::ControlStatus::OK : control::ControlStatus::NOT_FOUND;\n}\n\ncontrol::ControlStatus TimingController::reset_processor_timings(int processor_id)\n{\n    ELKLOG_LOG_DEBUG(\"reset_processor_timings called, returning \");\n    return reset_track_timings(processor_id);\n}\n\nstd::pair<control::ControlStatus, control::Timings> TimingController::_get_timings(int node) const\n{\n    if (_performance_timer->enabled())\n    {\n        auto timings = _performance_timer->timings_for_node(node);\n        if (timings.has_value())\n        {\n            return {control::ControlStatus::OK, to_external(timings.value())};\n        }\n        return {control::ControlStatus::NOT_FOUND, {0,0,0}};\n    }\n    return {control::ControlStatus::UNSUPPORTED_OPERATION, {0,0,0}};\n}\n\n} // end namespace sushi::internal::engine::controller_impl"
  },
  {
    "path": "src/engine/controller/timing_controller.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_TIMING_CONTROLLER_H\n#define SUSHI_TIMING_CONTROLLER_H\n\n#include \"sushi/control_interface.h\"\n\n#include \"engine/base_engine.h\"\n#include \"library/base_performance_timer.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nclass TimingController : public control::TimingController\n{\npublic:\n    explicit TimingController(BaseEngine* engine);\n\n    ~TimingController() override = default;\n\n    bool get_timing_statistics_enabled() const override;\n\n    void set_timing_statistics_enabled(bool enabled) override;\n\n    std::pair<control::ControlStatus, control::CpuTimings> get_engine_timings() const override;\n\n    std::pair<control::ControlStatus, control::Timings> get_track_timings(int track_id) const override;\n\n    std::pair<control::ControlStatus, control::Timings> get_processor_timings(int processor_id) const override;\n\n    control::ControlStatus reset_all_timings() override;\n\n    control::ControlStatus reset_track_timings(int track_id) override;\n\n    control::ControlStatus reset_processor_timings(int processor_id) override;\n\nprivate:\n    std::pair<control::ControlStatus, control::Timings> _get_timings(int node) const;\n\n    performance::BasePerformanceTimer*  _performance_timer;\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif // SUSHI_TIMING_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/controller/transport_controller.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface of sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"transport_controller.h\"\n\n#include \"controller_common.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"controller\");\n\nnamespace sushi::internal::engine::controller_impl {\n\nTransportController::TransportController(engine::BaseEngine* engine) : _engine(engine),\n                                                                       _transport(engine->transport()),\n                                                                       _event_dispatcher(engine->event_dispatcher())\n{}\n\nfloat TransportController::get_samplerate() const\n{\n    ELKLOG_LOG_DEBUG(\"get_samplerate called\");\n    return _engine->sample_rate();\n}\n\ncontrol::PlayingMode TransportController::get_playing_mode() const\n{\n    ELKLOG_LOG_DEBUG(\"get_playing_mode called\");\n    return to_external(_transport->playing_mode());\n}\n\nvoid TransportController::set_playing_mode(control::PlayingMode playing_mode)\n{\n    ELKLOG_LOG_DEBUG(\"set_playing_mode called\");\n    _event_dispatcher->post_event(std::make_unique<SetEnginePlayingModeStateEvent>(to_internal(playing_mode),\n                                                                                   IMMEDIATE_PROCESS));\n}\n\ncontrol::SyncMode TransportController::get_sync_mode() const\n{\n    ELKLOG_LOG_DEBUG(\"get_sync_mode called\");\n    return to_external(_transport->sync_mode());\n}\n\ncontrol::ControlStatus TransportController::set_sync_mode(control::SyncMode sync_mode)\n{\n    ELKLOG_LOG_DEBUG(\"set_sync_mode called\");\n    _event_dispatcher->post_event(std::make_unique<SetEngineSyncModeEvent>(to_internal(sync_mode),\n                                                                           IMMEDIATE_PROCESS));\n    return control::ControlStatus::OK;\n}\n\nfloat TransportController::get_tempo() const\n{\n    ELKLOG_LOG_DEBUG(\"get_tempo called\");\n    return _transport->current_tempo();\n}\n\ncontrol::ControlStatus TransportController::set_tempo(float tempo)\n{\n    ELKLOG_LOG_DEBUG(\"set_tempo called with tempo {}\", tempo);\n    _event_dispatcher->post_event(std::make_unique<SetEngineTempoEvent>(tempo, IMMEDIATE_PROCESS));\n    return control::ControlStatus::OK;\n}\n\ncontrol::TimeSignature TransportController::get_time_signature() const\n{\n    ELKLOG_LOG_DEBUG(\"get_time_signature called\");\n    return to_external(_transport->time_signature());\n}\n\ncontrol::ControlStatus TransportController::set_time_signature(control::TimeSignature signature)\n{\n    ELKLOG_LOG_DEBUG(\"set_time_signature called with signature {}/{}\", signature.numerator, signature.denominator);\n    _event_dispatcher->post_event(std::make_unique<SetEngineTimeSignatureEvent>(to_internal(signature), IMMEDIATE_PROCESS));\n    return control::ControlStatus::OK;\n}\n\n} // end namespace sushi::internal::engine::controller_impl"
  },
  {
    "path": "src/engine/controller/transport_controller.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Implementation of external control interface for sushi.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_TRANSPORT_CONTROLLER_H\n#define SUSHI_TRANSPORT_CONTROLLER_H\n\n#include \"sushi/control_interface.h\"\n\n#include \"engine/base_engine.h\"\n#include \"engine/base_event_dispatcher.h\"\n\nnamespace sushi::internal::engine::controller_impl {\n\nclass TransportController : public control::TransportController\n{\npublic:\n    explicit TransportController(BaseEngine* engine);\n\n    float get_samplerate() const override;\n\n    control::PlayingMode get_playing_mode() const override;\n\n    control::SyncMode get_sync_mode() const override;\n\n    control::TimeSignature get_time_signature() const override;\n\n    float get_tempo() const override;\n\n    control::ControlStatus set_sync_mode(control::SyncMode sync_mode) override;\n\n    void set_playing_mode(control::PlayingMode playing_mode) override;\n\n    control::ControlStatus set_tempo(float tempo) override;\n\n    control::ControlStatus set_time_signature(control::TimeSignature signature) override;\n\nprivate:\n    BaseEngine*                         _engine;\n    engine::Transport*                  _transport;\n    dispatcher::BaseEventDispatcher*    _event_dispatcher;\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n#endif // SUSHI_TRANSPORT_CONTROLLER_H\n"
  },
  {
    "path": "src/engine/event_dispatcher.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Implementation of Event Dispatcher\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"event_dispatcher.h\"\n#include \"engine/base_engine.h\"\n\nnamespace sushi::internal::dispatcher {\n\nconstexpr std::chrono::milliseconds THREAD_PERIODICITY = std::chrono::milliseconds(1);\nconstexpr auto WORKER_THREAD_PERIODICITY = std::chrono::milliseconds(1);\nconstexpr auto TIMING_UPDATE_INTERVAL = std::chrono::seconds(1);\nconstexpr auto PARAMETER_UPDATE_RATE = 10;\n// Rate limits broadcast parameter updates to 25 Hz\nconstexpr auto MAX_PARAMETER_UPDATE_INTERVAL = std::chrono::milliseconds(40);\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"event dispatcher\");\n\nEventDispatcher::EventDispatcher(engine::BaseEngine* engine,\n                                 RtSafeRtEventFifo* in_rt_queue,\n                                 RtSafeRtEventFifo* out_rt_queue) : _running{false},\n                                                                    _in_rt_queue{in_rt_queue},\n                                                                    _out_rt_queue{out_rt_queue},\n                                                                    _worker{engine, this},\n                                                                    _event_timer{engine->sample_rate()},\n                                                                    _parameter_manager{MAX_PARAMETER_UPDATE_INTERVAL,\n                                                                                       engine->processor_container()},\n                                                                    _parameter_update_count{0}\n{}\n\nEventDispatcher::~EventDispatcher()\n{\n    if (_running)\n    {\n        stop();\n    }\n\n    while (!_in_queue.empty())\n    {\n        // As each event goes out of scope, it's deleted.\n        auto event = _in_queue.pop();\n    }\n}\n\nvoid EventDispatcher::post_event(std::unique_ptr<Event> event)\n{\n    _in_queue.push(std::move(event));\n}\n\nvoid EventDispatcher::run()\n{\n    if (!_running)\n    {\n        _running = true;\n        _event_thread = std::thread(&EventDispatcher::_event_loop, this);\n        _worker.run();\n    }\n}\n\nvoid EventDispatcher::stop()\n{\n    _running = false;\n    _worker.stop();\n    if (_event_thread.joinable())\n    {\n        _event_thread.join();\n    }\n}\n\nStatus EventDispatcher::subscribe_to_keyboard_events(EventPoster* receiver)\n{\n    std::lock_guard<std::mutex> lock(_keyboard_listener_lock);\n\n    for (auto r : _keyboard_event_listeners)\n    {\n        if (r == receiver) return Status::ALREADY_SUBSCRIBED;\n    }\n    _keyboard_event_listeners.push_back(receiver);\n    return Status::OK;\n}\n\nStatus EventDispatcher::subscribe_to_parameter_change_notifications(EventPoster* receiver)\n{\n    std::lock_guard<std::mutex> lock(_parameter_listener_lock);\n\n    for (auto r : _parameter_change_listeners)\n    {\n        if (r == receiver) return Status::ALREADY_SUBSCRIBED;\n    }\n    _parameter_change_listeners.push_back(receiver);\n    return Status::OK;\n}\n\nStatus EventDispatcher::subscribe_to_engine_notifications(EventPoster*receiver)\n{\n    std::lock_guard<std::mutex> lock(_engine_listener_lock);\n\n    for (auto r : _engine_notification_listeners)\n    {\n        if (r == receiver) return Status::ALREADY_SUBSCRIBED;\n    }\n    _engine_notification_listeners.push_back(receiver);\n    return Status::OK;\n}\n\nint EventDispatcher::dispatch(std::unique_ptr<Event> event)\n{\n    int status = EventStatus::NOT_HANDLED;\n\n    if (event->process_asynchronously())\n    {\n        return _worker.dispatch(std::move(event));\n    }\n\n    if (event->is_parameter_change_event())\n    {\n        auto typed_event = static_cast<const ParameterChangeEvent*>(event.get());\n        _parameter_manager.mark_parameter_changed(typed_event->processor_id(),\n                                                  typed_event->parameter_id(),\n                                                  typed_event->time());\n    }\n\n    if (event->maps_to_rt_event())\n    {\n        auto [send_now, sample_offset] = _event_timer.sample_offset_from_realtime(event->time());\n        if (send_now)\n        {\n            if (_out_rt_queue->push(event->to_rt_event(sample_offset)))\n            {\n                status = EventStatus::HANDLED_OK;\n            }\n        }\n        else\n        {\n            _waiting_list.push_front(std::move(event));\n\n            // Dispatch will be called with this event again. When it HAS run, callback is called.\n            return EventStatus::QUEUED_HANDLING;\n        }\n    }\n\n    if (event->is_parameter_change_notification() || event->is_property_change_notification())\n    {\n        _publish_parameter_events(event.get());\n        status = EventStatus::HANDLED_OK;\n    }\n\n    if (event->is_engine_notification())\n    {\n        _handle_engine_notifications_internally(static_cast<EngineNotificationEvent*>(event.get()));\n        _publish_engine_notification_events(event.get());\n        status = EventStatus::HANDLED_OK;\n    }\n\n    if (status == EventStatus::HANDLED_OK)\n    {\n        if (event->completion_cb() != nullptr)\n        {\n            event->completion_cb()(event->callback_arg(), event.get(), status);\n        }\n    }\n    else\n    {\n        ELKLOG_LOG_ERROR(\"There should never be an unrecognized event.\");\n        // If there is one, the above event handling chain is broken.\n\n        assert(false);\n\n        status = EventStatus::UNRECOGNIZED_EVENT;\n    }\n\n    return status;\n}\n\nvoid EventDispatcher::_event_loop()\n{\n    do\n    {\n        auto start_time = std::chrono::steady_clock::now();\n\n        // Handle incoming Events\n        while (auto event = _next_event())\n        {\n            dispatch(std::move(event));\n        }\n\n        // Handle incoming RtEvents\n        while (!_in_rt_queue->empty())\n        {\n            RtEvent rt_event;\n            _in_rt_queue->pop(rt_event);\n            _process_rt_event(rt_event);\n        }\n\n        // Send updates for any parameters that have changed\n        if (_parameter_update_count++ >= PARAMETER_UPDATE_RATE)\n        {\n            _parameter_manager.output_parameter_notifications(this, _last_rt_event_time);\n            _parameter_update_count = 0;\n        }\n\n        std::this_thread::sleep_until(start_time + THREAD_PERIODICITY);\n    }\n    while (_running);\n}\n\nint EventDispatcher::_process_rt_event(RtEvent &rt_event)\n{\n    if (rt_event.type() == RtEventType::FLOAT_PARAMETER_CHANGE ||\n        rt_event.type() == RtEventType::INT_PARAMETER_CHANGE ||\n        rt_event.type() == RtEventType::BOOL_PARAMETER_CHANGE)\n    {\n        auto typed_event = rt_event.parameter_change_event();\n        _parameter_manager.mark_parameter_changed(typed_event->processor_id(), typed_event->param_id(), IMMEDIATE_PROCESS);\n        return EventStatus::HANDLED_OK;\n    }\n\n    Time timestamp = _event_timer.real_time_from_sample_offset(rt_event.sample_offset());\n    auto event = Event::from_rt_event(rt_event, timestamp);\n    if (event == nullptr)\n    {\n        switch (rt_event.type())\n        {\n            case RtEventType::SYNC:\n            {\n                auto typed_event = rt_event.syncronisation_event();\n                _event_timer.set_outgoing_time(typed_event->timestamp());\n                _last_rt_event_time = typed_event->timestamp();\n                return EventStatus::HANDLED_OK;\n            }\n\n            default:\n                return EventStatus::UNRECOGNIZED_EVENT;\n        }\n    }\n\n    if (event->is_keyboard_event())\n    {\n        _publish_keyboard_events(event.get());\n    }\n    else if (event->is_engine_notification())\n    {\n        _publish_engine_notification_events(event.get());\n    }\n\n    if (event->process_asynchronously())\n    {\n        return _worker.dispatch(std::move(event));\n    }\n\n    return EventStatus::HANDLED_OK;\n}\n\nstd::unique_ptr<Event> EventDispatcher::_next_event()\n{\n    std::unique_ptr<Event> event = nullptr;\n    if (!_waiting_list.empty())\n    {\n        event = std::move(_waiting_list.back());\n        _waiting_list.pop_back();\n    }\n    else if (!_in_queue.empty())\n    {\n        event = _in_queue.pop();\n    }\n\n    return event;\n}\n\nvoid EventDispatcher::_publish_keyboard_events(Event* event)\n{\n    std::lock_guard<std::mutex> lock(_keyboard_listener_lock);\n\n    for (auto& listener : _keyboard_event_listeners)\n    {\n        listener->process(event);\n    }\n}\n\nvoid EventDispatcher::_publish_parameter_events(Event* event)\n{\n    std::lock_guard<std::mutex> lock(_parameter_listener_lock);\n\n    for (auto& listener : _parameter_change_listeners)\n    {\n        listener->process(event);\n    }\n}\n\nvoid EventDispatcher::_publish_engine_notification_events(Event* event)\n{\n    std::lock_guard<std::mutex> lock(_engine_listener_lock);\n\n    for (auto& listener : _engine_notification_listeners)\n    {\n        listener->process(event);\n    }\n}\n\nStatus EventDispatcher::unsubscribe_from_keyboard_events(EventPoster* receiver)\n{\n    std::lock_guard<std::mutex> lock(_keyboard_listener_lock);\n\n    for (auto i = _keyboard_event_listeners.begin(); i != _keyboard_event_listeners.end(); ++i)\n    {\n        if (*i == receiver)\n        {\n            _keyboard_event_listeners.erase(i);\n            return Status::OK;\n        }\n    }\n    return Status::UNKNOWN_POSTER;\n}\n\nStatus EventDispatcher::unsubscribe_from_parameter_change_notifications(EventPoster* receiver)\n{\n    std::lock_guard<std::mutex> lock(_parameter_listener_lock);\n\n    for (auto i = _parameter_change_listeners.begin(); i != _parameter_change_listeners.end(); ++i)\n    {\n        if (*i == receiver)\n        {\n            _parameter_change_listeners.erase(i);\n            return Status::OK;\n        }\n    }\n    return Status::UNKNOWN_POSTER;\n}\n\nStatus EventDispatcher::unsubscribe_from_engine_notifications(EventPoster* receiver)\n{\n    std::lock_guard<std::mutex> lock(_engine_listener_lock);\n\n    for (auto i = _engine_notification_listeners.begin(); i != _engine_notification_listeners.end(); ++i)\n    {\n        if (*i == receiver)\n        {\n            _engine_notification_listeners.erase(i);\n            return Status::OK;\n        }\n    }\n    return Status::UNKNOWN_POSTER;\n}\n\nvoid EventDispatcher::_handle_engine_notifications_internally(EngineNotificationEvent* event)\n{\n    if (event->is_audio_graph_notification())\n    {\n        auto typed_event = static_cast<AudioGraphNotificationEvent*>(event);\n        switch (typed_event->action())\n        {\n            case AudioGraphNotificationEvent::Action::PROCESSOR_CREATED:\n                _parameter_manager.track_parameters(typed_event->processor());\n                break;\n\n            case AudioGraphNotificationEvent::Action::TRACK_CREATED:\n                _parameter_manager.track_parameters(typed_event->track());\n                break;\n\n            case AudioGraphNotificationEvent::Action::PROCESSOR_UPDATED:\n                _parameter_manager.mark_processor_changed(typed_event->processor(), typed_event->time());\n                break;\n\n            case AudioGraphNotificationEvent::Action::PROCESSOR_DELETED:\n                _parameter_manager.untrack_parameters(typed_event->processor());\n                break;\n\n            case AudioGraphNotificationEvent::Action::TRACK_DELETED:\n                _parameter_manager.untrack_parameters(typed_event->track());\n                break;\n\n            default:\n                break;\n        }\n    }\n}\n\nvoid Worker::run()\n{\n    _running = true;\n    _worker_thread = std::thread(&Worker::_worker, this);\n}\n\nvoid Worker::stop()\n{\n    _running = false;\n    if (_worker_thread.joinable())\n    {\n        _worker_thread.join();\n    }\n}\n\nint Worker::dispatch(std::unique_ptr<Event> event)\n{\n    _queue.push(std::move(event));\n\n    return EventStatus::QUEUED_HANDLING;\n}\n\nvoid Worker::_worker()\n{\n    std::chrono::time_point<std::chrono::steady_clock, std::chrono::nanoseconds> timing_update_counter;\n    do\n    {\n        auto start_time = std::chrono::steady_clock::now();\n        while (!_queue.empty())\n        {\n            int status = EventStatus::UNRECOGNIZED_EVENT;\n            auto event = _queue.pop();\n\n            if (event->is_engine_event())\n            {\n                auto typed_event = static_cast<EngineEvent*>(event.get());\n                status = typed_event->execute(_engine);\n            }\n\n            if (event->is_async_work_event())\n            {\n                auto typed_event = static_cast<AsynchronousWorkEvent*>(event.get());\n                auto response_event = typed_event->execute();\n                if (response_event != nullptr)\n                {\n                    _dispatcher->post_event(std::move(response_event));\n                }\n            }\n\n            // This is a synchronous call to the completion callback,\n            // meaning it's fine that the event then goes out of scope and is deleted.\n            if (event->completion_cb() != nullptr)\n            {\n                event->completion_cb()(event->callback_arg(), event.get(), status);\n            }\n        }\n\n        if (start_time > timing_update_counter + TIMING_UPDATE_INTERVAL)\n        {\n            timing_update_counter = start_time;\n            _engine->update_timings();\n        }\n\n        std::this_thread::sleep_until(start_time + WORKER_THREAD_PERIODICITY);\n    }\n    while (_running);\n}\n\n} // end namespace sushi::internal::dispatcher\n"
  },
  {
    "path": "src/engine/event_dispatcher.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Implementation of Event Dispatcher\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_EVENT_DISPATCHER_H\n#define SUSHI_EVENT_DISPATCHER_H\n\n#include <deque>\n#include <vector>\n#include <thread>\n\n#include \"engine/base_event_dispatcher.h\"\n#include \"engine/base_engine.h\"\n#include \"engine/event_timer.h\"\n#include \"engine/parameter_manager.h\"\n#include \"library/synchronised_fifo.h\"\n#include \"library/rt_event_fifo.h\"\n#include \"library/event_interface.h\"\n\nnamespace sushi::internal::engine {\n    class BaseEngine;\n}\n\nnamespace sushi::internal::dispatcher {\n\nclass BaseEventDispatcher;\n\nclass Accessor;\nclass WorkerAccessor;\n\nusing EventQueue = SynchronizedQueue<std::unique_ptr<Event>>;\n\n/**\n * @brief Low priority worker for handling possibly time consuming tasks like\n * instantiating plugins or do asynchronous work from processors.\n */\nclass Worker\n{\npublic:\n    Worker(engine::BaseEngine* engine, BaseEventDispatcher* dispatcher) : _engine(engine),\n                                                                          _dispatcher(dispatcher),\n                                                                          _running(false) {}\n\n    virtual ~Worker() = default;\n\n    void run();\n    void stop();\n\n    int dispatch(std::unique_ptr<Event> event);\n\nprivate:\n    friend Accessor;\n    friend WorkerAccessor;\n\n    engine::BaseEngine*         _engine;\n    BaseEventDispatcher*        _dispatcher;\n\n    void                        _worker();\n    std::thread                 _worker_thread;\n    std::atomic<bool>           _running;\n\n    EventQueue _queue;\n};\n\nclass EventDispatcher : public BaseEventDispatcher\n{\npublic:\n    EventDispatcher(engine::BaseEngine* engine, RtSafeRtEventFifo* in_rt_queue,  RtSafeRtEventFifo* out_rt_queue);\n\n    ~EventDispatcher() override;\n\n    void run() override;\n    void stop() override;\n\n    void post_event(std::unique_ptr<Event> event) override;\n\n    Status subscribe_to_keyboard_events(EventPoster* receiver) override;\n    Status subscribe_to_parameter_change_notifications(EventPoster* receiver) override;\n    Status subscribe_to_engine_notifications(EventPoster* receiver) override;\n\n    Status unsubscribe_from_keyboard_events(EventPoster* receiver) override;\n    Status unsubscribe_from_parameter_change_notifications(EventPoster* receiver) override;\n    Status unsubscribe_from_engine_notifications(EventPoster* receiver) override;\n\n    void set_sample_rate(float sample_rate) override {_event_timer.set_sample_rate(sample_rate);}\n    void set_time(Time timestamp) override {_event_timer.set_incoming_time(timestamp);}\n\n    int dispatch(std::unique_ptr<Event> event) override;\n\nprivate:\n    friend Accessor;\n\n    void _event_loop();\n\n    int _process_rt_event(RtEvent& rt_event);\n\n    std::unique_ptr<Event> _next_event();\n\n    void _publish_keyboard_events(Event* event);\n    void _publish_parameter_events(Event* event);\n    void _publish_engine_notification_events(Event* event);\n    void _handle_engine_notifications_internally(EngineNotificationEvent* event);\n\n    std::atomic<bool>           _running;\n    std::thread                 _event_thread;\n\n    EventQueue _in_queue;\n\n    RtSafeRtEventFifo*          _in_rt_queue;\n    RtSafeRtEventFifo*          _out_rt_queue;\n\n    std::deque<std::unique_ptr<Event>> _waiting_list;\n\n    Worker                      _worker;\n    event_timer::EventTimer     _event_timer;\n    ParameterManager            _parameter_manager;\n    int                         _parameter_update_count;\n    Time                        _last_rt_event_time{};\n\n    std::vector<EventPoster*> _keyboard_event_listeners;\n    std::vector<EventPoster*> _parameter_change_listeners;\n    std::vector<EventPoster*> _engine_notification_listeners;\n\n    std::mutex _keyboard_listener_lock;\n    std::mutex _parameter_listener_lock;\n    std::mutex _engine_listener_lock;\n};\n\n} // end namespace sushi::internal::dispatcher\n\n#endif // SUSHI_EVENT_DISPATCHER_H\n"
  },
  {
    "path": "src/engine/event_timer.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Object to map between rt time and real time\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <algorithm>\n#include <cmath>\n#include <cassert>\n\n#include \"sushi/constants.h\"\n\n#include \"event_timer.h\"\n\nnamespace sushi::internal::event_timer {\n\nusing namespace std::chrono_literals;\nconstexpr float MICROSECONDS = static_cast<float>(std::chrono::microseconds(1s).count());\n\ninline Time calc_chunk_time(float samplerate)\n{\n     return std::chrono::microseconds(static_cast<int64_t>(std::round(MICROSECONDS / samplerate * AUDIO_CHUNK_SIZE)));\n}\n\nEventTimer::EventTimer(float default_sample_rate) : _sample_rate{default_sample_rate},\n                                                    _chunk_time{calc_chunk_time(default_sample_rate)}\n{\n    assert(EventTimer::_incoming_chunk_time.is_lock_free());\n}\n\nstd::pair<bool, int> EventTimer::sample_offset_from_realtime(Time timestamp) const\n{\n    auto diff = timestamp - _incoming_chunk_time.load();\n    if (diff < _chunk_time)\n    {\n        int64_t offset = (AUDIO_CHUNK_SIZE * diff) / _chunk_time;\n        return std::make_pair(true, static_cast<int>(std::max(int64_t{0}, offset)));\n    }\n    else\n    {\n        return std::make_pair(false, 0);\n    }\n}\n\nTime EventTimer::real_time_from_sample_offset(int offset) const\n{\n    return _outgoing_chunk_time + offset * _chunk_time / AUDIO_CHUNK_SIZE;\n}\n\nvoid EventTimer::set_sample_rate(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    _chunk_time = calc_chunk_time(sample_rate);\n}\n\n} // end sushi::internal::event_timer\n"
  },
  {
    "path": "src/engine/event_timer.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Object to map between rt time and real time\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_EVENT_TIMER_H\n#define SUSHI_EVENT_TIMER_H\n\n#include <atomic>\n#include <tuple>\n\n#include \"sushi/sushi_time.h\"\n\nnamespace sushi::internal::event_timer {\n\nclass EventTimer\n{\npublic:\n    explicit EventTimer(float default_sample_rate);\n\n    ~EventTimer() = default;\n\n    /**\n     * @brief Convert a timestamp to a sample offset within the next chunk\n     * @param timestamp A real time timestamp\n     * @return If the timestamp falls withing the next chunk, the function\n     *         returns true and the offset in samples. If the timestamp\n     *         lies further in the future, the function returns false and\n     *         the returned offset is not valid.\n     */\n    std::pair<bool, int> sample_offset_from_realtime(Time timestamp) const;\n\n    /**\n     * @brief Convert a sample offset to real time.\n     * @param offset Offset in samples\n     * @return Timestamp\n     */\n    Time real_time_from_sample_offset(int offset) const;\n\n    /**\n     * @brief Set the samplerate of the converter.\n     * @param sample_rate Samplerate in Hz\n     */\n    void set_sample_rate(float sample_rate);\n\n    /**\n     * @brief Called from the rt part when all rt events have been processed, essentially\n     *        closing the window for events for this chunk\n     * @param timestamp The time when the currently processed chunk is outputted\n     */\n    void set_incoming_time(Time timestamp) {_incoming_chunk_time.store(timestamp + _chunk_time);}\n\n    /**\n     * @brief Called from the event thread when all outgoing events from a chunk have\n     *        been processed\n     * @param timestamp of the previously processed audio chunk\n     */\n    void set_outgoing_time(Time timestamp) {_outgoing_chunk_time = timestamp + _chunk_time;}\n\nprivate:\n    float               _sample_rate;\n    Time                _chunk_time;\n    Time                _outgoing_chunk_time{IMMEDIATE_PROCESS};\n    std::atomic<Time>   _incoming_chunk_time{IMMEDIATE_PROCESS};\n};\n\n} // end namespace sushi::internal::event_timer\n\n#endif // SUSHI_EVENT_TIMER_H\n"
  },
  {
    "path": "src/engine/host_control.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Object passed to processors to allow them access to the engine\n *        to query things like time, tempo and to post non rt events.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_HOST_CONTROL_H\n#define SUSHI_HOST_CONTROL_H\n\n#include \"base_event_dispatcher.h\"\n#include \"engine/transport.h\"\n#include \"engine/plugin_library.h\"\n\nnamespace sushi::internal {\n\nclass HostControl\n{\npublic:\n    HostControl(dispatcher::BaseEventDispatcher* event_dispatcher,\n                engine::Transport* transport,\n                engine::PluginLibrary* library) :\n                    _event_dispatcher(event_dispatcher),\n                    _transport(transport),\n                    _plugin_library(library)\n    {}\n\n    /**\n     * @brief Post an event into the dispatcher's queue\n     *\n     * @param event the event\n     */\n    void post_event(std::unique_ptr<Event> event)\n    {\n        _event_dispatcher->post_event(std::move(event));\n    }\n\n    /**\n     * @brief Get the engine's transport interface\n     */\n    const engine::Transport* transport()\n    {\n        return _transport;\n    }\n\n    /**\n     * @brief Convert a relative plugin path to an absolute path,\n     *        if a base plugin path has been set.\n     *\n     * @param path Relative path to plugin inside the base plugin folder.\n     *             It is the caller's responsibility\n     *             to ensure that this is a proper relative path (not starting with \"/\").\n     *\n     * @return Absolute path of the plugin\n     */\n    std::string to_absolute_path(const std::string& path)\n    {\n        return _plugin_library->to_absolute_path(path);\n    }\n\nprotected:\n    dispatcher::BaseEventDispatcher* _event_dispatcher;\n    engine::Transport*               _transport;\n    engine::PluginLibrary*           _plugin_library;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_HOST_CONTROL_H\n"
  },
  {
    "path": "src/engine/json_configurator.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Class to configure the audio engine using Json config files.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_TYPE_LIMITS\nELK_DISABLE_CONDITIONAL_EXPRESSION_IS_CONSTANT\nELK_DISABLE_COMPARISON_CALLS_NAME_RECURSIVELY\n#include \"rapidjson/error/en.h\"\n#include \"rapidjson/stringbuffer.h\"\n#include \"rapidjson/schema.h\"\nELK_POP_WARNING\n\n#include \"elklog/static_logger.h\"\n\n#include \"json_configurator.h\"\n\nnamespace sushi::internal::jsonconfig {\n\nusing namespace engine;\nusing namespace midi_dispatcher;\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"jsonconfig\");\n\nconstexpr int ERROR_DISPLAY_CHARS = 50;\n\nconst char* section_name(JsonSection section)\n{\n    switch (section)\n    {\n        case JsonSection::HOST_CONFIG:   return \"host_config\";\n        case JsonSection::TRACKS:        return \"tracks\";\n        case JsonSection::PRE_TRACK:     return \"pre_track\";\n        case JsonSection::POST_TRACK:    return \"post_track\";\n        case JsonSection::MIDI:          return \"midi\";\n        case JsonSection::OSC:           return \"osc\";\n        case JsonSection::CV_GATE:       return \"cv_control\";\n        case JsonSection::EVENTS:        return \"events\";\n        case JsonSection::STATE:         return \"initial_state\";\n        default:                         return nullptr;\n    }\n}\n\nconst char* section_schema(JsonSection section)\n{\n    switch(section)\n    {\n        case JsonSection::HOST_CONFIG:  return\n            #include \"json_schemas/host_config_schema.json\"\n            ;\n        case JsonSection::TRACKS:\n        case JsonSection::PRE_TRACK:\n        case JsonSection::POST_TRACK:  return\n            #include \"json_schemas/tracks_schema.json\"\n            ;\n        case JsonSection::MIDI: return\n            #include \"json_schemas/midi_schema.json\"\n            ;\n        case JsonSection::OSC: return\n            #include \"json_schemas/osc_schema.json\"\n            ;\n        case JsonSection::CV_GATE: return\n            #include \"json_schemas/cv_gate_schema.json\"\n            ;\n        case JsonSection::EVENTS: return\n            #include \"json_schemas/events_schema.json\"\n            ;\n        case JsonSection::STATE: return\n            #include \"json_schemas/state_schema.json\"\n            ;\n        default:\n            return nullptr;\n    }\n}\n\nstd::pair<JsonConfigReturnStatus, ControlConfig> JsonConfigurator::load_control_config()\n{\n    ControlConfig control_config;\n    auto [status, host_config] = _parse_section(JsonSection::HOST_CONFIG);\n    if (status != JsonConfigReturnStatus::OK)\n    {\n        return {status, control_config};\n    }\n\n    if (host_config.HasMember(\"cv_inputs\"))\n    {\n        control_config.cv_inputs = host_config[\"cv_inputs\"].GetInt();\n    }\n\n    if (host_config.HasMember(\"cv_outputs\"))\n    {\n        control_config.cv_outputs = host_config[\"cv_outputs\"].GetInt();\n    }\n\n    if (host_config.HasMember(\"midi_inputs\"))\n    {\n        control_config.midi_inputs = host_config[\"midi_inputs\"].GetInt();\n    }\n\n    if (host_config.HasMember(\"midi_outputs\"))\n    {\n        control_config.midi_outputs = host_config[\"midi_outputs\"].GetInt();\n    }\n\n    if (host_config.HasMember(\"rt_midi_input_mappings\"))\n    {\n        for (auto& mapping : host_config[\"rt_midi_input_mappings\"].GetArray())\n        {\n            auto rt_midi_device = mapping[\"rt_midi_device\"].GetInt();\n            auto sushi_midi_port = mapping[\"sushi_midi_port\"].GetInt();\n            bool virtual_port = false;\n            if (mapping.HasMember(\"virtual_port\"))\n            {\n                virtual_port = mapping[\"virtual_port\"].GetBool();\n            }\n            control_config.rt_midi_input_mappings.emplace_back(std::make_tuple(rt_midi_device, sushi_midi_port, virtual_port));\n        }\n    }\n\n    if (host_config.HasMember(\"rt_midi_output_mappings\"))\n    {\n        for (auto& mapping : host_config[\"rt_midi_output_mappings\"].GetArray())\n        {\n            auto rt_midi_device = mapping[\"rt_midi_device\"].GetInt();\n            auto sushi_midi_port = mapping[\"sushi_midi_port\"].GetInt();\n            bool virtual_port = false;\n            if (mapping.HasMember(\"virtual_port\"))\n            {\n                virtual_port = mapping[\"virtual_port\"].GetBool();\n            }\n            control_config.rt_midi_output_mappings.emplace_back(std::make_tuple(rt_midi_device, sushi_midi_port, virtual_port));\n        }\n    }\n\n    return {JsonConfigReturnStatus::OK, control_config};\n}\n\nJsonConfigReturnStatus JsonConfigurator::load_host_config()\n{\n    auto [status, host_config] = _parse_section(JsonSection::HOST_CONFIG);\n    if (status != JsonConfigReturnStatus::OK)\n    {\n        return status;\n    }\n\n    float sample_rate = host_config[\"samplerate\"].GetFloat();\n    ELKLOG_LOG_INFO(\"Setting engine sample rate to {}\", sample_rate);\n    _engine->set_sample_rate(sample_rate);\n\n    if (host_config.HasMember(\"tempo\"))\n    {\n        float tempo = host_config[\"tempo\"].GetFloat();\n        ELKLOG_LOG_INFO(\"Setting engine tempo to {}\", sample_rate);\n        _engine->set_tempo(tempo);\n    }\n\n    if (host_config.HasMember(\"time_signature\"))\n    {\n        const auto& sig = host_config[\"time_signature\"].GetObject();\n        int numerator = sig[\"numerator\"].GetInt();\n        int denominator = sig[\"denominator\"].GetInt();\n\n        ELKLOG_LOG_INFO(\"Setting engine time signature to {}/{}\", numerator, denominator);\n        _engine->set_time_signature({numerator, denominator});\n    }\n\n    if (host_config.HasMember(\"playing_mode\"))\n    {\n        PlayingMode mode;\n        if (host_config[\"playing_mode\"] == \"stopped\")\n        {\n            mode = PlayingMode::STOPPED;\n        }\n        else\n        {\n            mode = PlayingMode::PLAYING;\n        }\n        ELKLOG_LOG_INFO(\"Setting engine playing mode to {}\", mode == PlayingMode::PLAYING ? \"playing \" : \"stopped\");\n        _engine->set_transport_mode(mode);\n    }\n\n    if (host_config.HasMember(\"tempo_sync\"))\n    {\n        SyncMode mode;\n        if (host_config[\"tempo_sync\"] == \"ableton_link\")\n        {\n            mode = SyncMode::ABLETON_LINK;\n        }\n        else if (host_config[\"tempo_sync\"] == \"midi\")\n        {\n            mode = SyncMode::MIDI;\n        }\n        else if (host_config[\"tempo_sync\"] == \"gate\")\n        {\n            mode = SyncMode::GATE_INPUT;\n        }\n        else\n        {\n            mode = SyncMode::INTERNAL;\n        }\n        ELKLOG_LOG_INFO(\"Setting engine tempo sync mode to {}\", mode == SyncMode::ABLETON_LINK? \"Ableton Link\" : (\n                                                               mode == SyncMode::MIDI ? \"external Midi\" : (\n                                                               mode == SyncMode::GATE_INPUT? \"Gate input\" : \"internal\")));\n        _engine->set_tempo_sync_mode(mode);\n    }\n\n    if (host_config.HasMember(\"audio_clip_detection\"))\n    {\n        const auto& clip_det = host_config[\"audio_clip_detection\"].GetObject();\n        if (clip_det.HasMember(\"inputs\"))\n        {\n            _engine->enable_input_clip_detection(clip_det[\"inputs\"].GetBool());\n            ELKLOG_LOG_INFO(\"Setting engine input clip detection {}\", clip_det[\"inputs\"].GetBool() ? \"enabled\" : \"disabled\");\n        }\n        if (clip_det.HasMember(\"outputs\"))\n        {\n            _engine->enable_output_clip_detection(clip_det[\"outputs\"].GetBool());\n            ELKLOG_LOG_INFO(\"Setting engine output clip detection {}\", clip_det[\"outputs\"].GetBool() ? \"enabled\" : \"disabled\");\n        }\n    }\n\n    if (host_config.HasMember(\"master_limiter\"))\n    {\n        _engine->enable_master_limiter(host_config[\"master_limiter\"].GetBool());\n        ELKLOG_LOG_INFO(\"Enable master limiter set to {}\", host_config[\"master_limiter\"].GetBool());\n    }\n\n    return JsonConfigReturnStatus::OK;\n}\n\nJsonConfigReturnStatus JsonConfigurator::load_tracks()\n{\n    auto [status, tracks] = _parse_section(JsonSection::TRACKS);\n    if (status != JsonConfigReturnStatus::OK)\n    {\n        return status;\n    }\n\n    for (auto& track : tracks.GetArray())\n    {\n        status = _make_track(track, TrackType::REGULAR);\n        if (status != JsonConfigReturnStatus::OK)\n        {\n            return status;\n        }\n    }\n\n    auto [pre_track_status, pre_track] = _parse_section(JsonSection::PRE_TRACK);\n    if (pre_track_status == JsonConfigReturnStatus::OK)\n    {\n        status = _make_track(pre_track, TrackType::PRE);\n        if (status != JsonConfigReturnStatus::OK)\n        {\n            return status;\n        }\n    }\n\n    auto [post_track_status, post_track] = _parse_section(JsonSection::POST_TRACK);\n    if (post_track_status == JsonConfigReturnStatus::OK)\n    {\n        status = _make_track(post_track, TrackType::POST);\n        if (status != JsonConfigReturnStatus::OK)\n        {\n            return status;\n        }\n    }\n\n    return status;\n}\n\nJsonConfigReturnStatus JsonConfigurator::load_midi()\n{\n    auto [midi_section_status, midi] = _parse_section(JsonSection::MIDI);\n    if (midi_section_status != JsonConfigReturnStatus::OK)\n    {\n        return midi_section_status;\n    }\n\n    if (midi.HasMember(\"track_connections\"))\n    {\n        for (const auto& con : midi[\"track_connections\"].GetArray())\n        {\n            bool raw_midi = con[\"raw_midi\"].GetBool();\n\n            auto track_name = con[\"track\"].GetString();\n            const auto track = _processor_container->track(track_name);\n            if (track == nullptr)\n            {\n                ELKLOG_LOG_ERROR(\"Invalid plugin track \\\"{}\\\" for midi \"\n                                \"track connection in Json config file.\", con[\"track\"].GetString());\n                return JsonConfigReturnStatus::INVALID_TRACK_NAME;\n            }\n\n            MidiDispatcherStatus res;\n            if (raw_midi)\n            {\n                res = _midi_dispatcher->connect_raw_midi_to_track(con[\"port\"].GetInt(),\n                                                                  track->id(),\n                                                                  _get_midi_channel(con[\"channel\"]));\n            }\n            else\n            {\n                res = _midi_dispatcher->connect_kb_to_track(con[\"port\"].GetInt(),\n                                                            track->id(),\n                                                            _get_midi_channel(con[\"channel\"]));\n            }\n\n            if (res != MidiDispatcherStatus::OK)\n            {\n                if (res == MidiDispatcherStatus::INVALID_MIDI_INPUT)\n                {\n                    ELKLOG_LOG_ERROR(\"Invalid port \\\"{}\\\" specified specified for midi \"\n                                           \"channel connections in Json Config file.\", con[\"port\"].GetInt());\n                    return JsonConfigReturnStatus::INVALID_MIDI_PORT;\n                }\n            }\n        }\n    }\n\n    if (midi.HasMember(\"track_out_connections\"))\n    {\n        for (const auto& con : midi[\"track_out_connections\"].GetArray())\n        {\n            auto track_name = con[\"track\"].GetString();\n            const auto track = _processor_container->track(track_name);\n            if (track == nullptr)\n            {\n                ELKLOG_LOG_ERROR(\"Invalid plugin track \\\"{}\\\" for midi \"\n                                \"track connection in Json config file.\", con[\"track\"].GetString());\n                return JsonConfigReturnStatus::INVALID_TRACK_NAME;\n            }\n\n            auto res = _midi_dispatcher->connect_track_to_output(con[\"port\"].GetInt(),\n                                                                 track->id(),\n                                                                 _get_midi_channel(con[\"channel\"]));\n            if (res != MidiDispatcherStatus::OK)\n            {\n                if (res == MidiDispatcherStatus::INVALID_MIDI_OUTPUT)\n                {\n                    ELKLOG_LOG_ERROR(\"Invalid port \\\"{}\\\" specified for midi \"\n                                           \"channel connections in Json Config file.\", con[\"port\"].GetInt());\n                    return JsonConfigReturnStatus::INVALID_MIDI_PORT;\n                }\n                else if (res == MidiDispatcherStatus::INVAlID_CHANNEL)\n                {\n                    ELKLOG_LOG_ERROR(\"Invalid channel \\\"{}\\\" specified for midi \"\n                                    \"channel connections in Json Config file.\", con[\"channel\"].GetInt());\n                    return JsonConfigReturnStatus::INVALID_MIDI_PORT;\n                }\n            }\n        }\n    }\n\n    if (midi.HasMember(\"program_change_connections\"))\n    {\n        for (const auto& con : midi[\"program_change_connections\"].GetArray())\n        {\n            auto processor_name = con[\"plugin\"].GetString();\n            const auto processor = _processor_container->processor(processor_name);\n            if (processor == nullptr)\n            {\n                ELKLOG_LOG_ERROR(\"Invalid plugin \\\"{}\\\" for MIDI program change \"\n                                \"connection in Json config file.\", processor_name);\n                return JsonConfigReturnStatus::INVALID_PLUGIN_NAME;\n            }\n\n            auto res = _midi_dispatcher->connect_pc_to_processor(con[\"port\"].GetInt(),\n                                                                 processor->id(),\n                                                                 _get_midi_channel(con[\"channel\"]));\n            if (res != MidiDispatcherStatus::OK)\n            {\n                if (res == MidiDispatcherStatus::INVALID_MIDI_INPUT)\n                {\n                    ELKLOG_LOG_ERROR(\"Invalid port \\\"{}\\\" specified specified for MIDI program change \"\n                                   \"channel connections in Json Config file.\", con[\"port\"].GetInt());\n                    return JsonConfigReturnStatus::INVALID_MIDI_PORT;\n                }\n            }\n        }\n    }\n\n    if (midi.HasMember(\"cc_mappings\"))\n    {\n        for (const auto& cc_map : midi[\"cc_mappings\"].GetArray())\n        {\n            bool is_relative = false;\n            if (cc_map.HasMember(\"mode\"))\n            {\n                auto cc_mode = std::string(cc_map[\"mode\"].GetString());\n                is_relative = (cc_mode.compare(\"relative\") == 0);\n            }\n\n            auto processor_name = cc_map[\"plugin_name\"].GetString();\n\n            const auto processor = _processor_container->processor(processor_name);\n            if (processor == nullptr)\n            {\n                ELKLOG_LOG_ERROR(\"Invalid plugin \\\"{}\\\" for MIDI program change \"\n                                \"connection in Json config file.\", processor_name);\n                return JsonConfigReturnStatus::INVALID_PLUGIN_NAME;\n            }\n\n            auto parameter_name = cc_map[\"parameter_name\"].GetString();\n            const auto parameter = processor->parameter_from_name(parameter_name);\n            if (parameter == nullptr)\n            {\n                return JsonConfigReturnStatus::INVALID_PARAMETER;\n            }\n\n            auto res = _midi_dispatcher->connect_cc_to_parameter(cc_map[\"port\"].GetInt(),\n                                                                 processor->id(),\n                                                                 parameter->id(),\n                                                                 cc_map[\"cc_number\"].GetInt(),\n                                                                 cc_map[\"min_range\"].GetFloat(),\n                                                                 cc_map[\"max_range\"].GetFloat(),\n                                                                 is_relative,\n                                                                 _get_midi_channel(cc_map[\"channel\"]));\n            if (res != MidiDispatcherStatus::OK)\n            {\n                if (res == MidiDispatcherStatus::INVALID_MIDI_INPUT)\n                {\n                    ELKLOG_LOG_ERROR(\"Invalid port \\\"{}\\\" specified \"\n                                           \"for midi cc mappings in Json Config file.\", cc_map[\"port\"].GetInt());\n                    return JsonConfigReturnStatus::INVALID_MIDI_PORT;\n                }\n                if (res == MidiDispatcherStatus::INVALID_PROCESSOR)\n                {\n                    ELKLOG_LOG_ERROR(\"Invalid plugin name \\\"{}\\\" specified \"\n                                           \"for midi cc mappings in Json Config file.\", cc_map[\"plugin_name\"].GetString());\n                    return JsonConfigReturnStatus::INVALID_TRACK_NAME;\n                }\n                ELKLOG_LOG_ERROR(\"Invalid parameter name \\\"{}\\\" specified for plugin \\\"{}\\\" for midi cc mappings.\",\n                                                                            cc_map[\"parameter_name\"].GetString(),\n                                                                            cc_map[\"plugin_name\"].GetString());\n                return JsonConfigReturnStatus::INVALID_PARAMETER;\n            }\n        }\n    }\n\n    if (midi.HasMember(\"clock_output\"))\n    {\n        for (const auto& port : midi[\"clock_output\"][\"enabled_ports\"].GetArray())\n        {\n            auto midi_clock_status = _midi_dispatcher->enable_midi_clock(true, port.GetInt());\n            if (midi_clock_status != midi_dispatcher::MidiDispatcherStatus::OK)\n            {\n                ELKLOG_LOG_ERROR(\"Failed to enable midi clock output on port {}\", port.GetInt());\n                return JsonConfigReturnStatus::INVALID_MIDI_PORT;\n            }\n        }\n    }\n\n    return JsonConfigReturnStatus::OK;\n}\n\nJsonConfigReturnStatus JsonConfigurator::load_osc()\n{\n    auto [status, osc_config] = _parse_section(JsonSection::OSC);\n    if (status != JsonConfigReturnStatus::OK)\n    {\n        return status;\n    }\n\n    if (!_osc_frontend)\n    {\n        ELKLOG_LOG_WARNING(\"OSC not enabled\");\n        return JsonConfigReturnStatus::INVALID_CONFIGURATION;\n    }\n\n    if (osc_config.HasMember(\"enable_all_processor_outputs\"))\n    {\n        bool enabled = osc_config[\"enable_all_processor_outputs\"].GetBool();\n        if (enabled)\n        {\n            _osc_frontend->set_connect_from_all_parameters(true);\n        }\n        else // While the current default is off, it may not always be, so why not have this wired up.\n        {\n            _osc_frontend->set_connect_from_all_parameters(false);\n        }\n        ELKLOG_LOG_INFO(\"Broadcasting of all processor parameter state is: {}\", enabled ? \"enabled\" : \"disabled\");\n    }\n\n    if (osc_config.HasMember(\"enabled_processor_outputs\"))\n    {\n        for (const auto& osc_out : osc_config[\"enabled_processor_outputs\"].GetArray())\n        {\n            auto processor_name = osc_out[\"processor\"].GetString();\n            auto res = _osc_frontend->connect_from_processor_parameters(processor_name);\n            if (res != true)\n            {\n                ELKLOG_LOG_ERROR(\"Failed to enable osc parameter output on processor {}\", processor_name);\n            }\n\n            if (osc_out.HasMember(\"parameter_blocklist\"))\n            {\n                for (const auto& parameter : osc_out[\"parameter_blocklist\"].GetArray())\n                {\n                    auto parameter_name = parameter[\"parameter\"].GetString();\n                    _osc_frontend->disconnect_from_parameter(processor_name, parameter_name);\n                }\n            }\n        }\n    }\n\n    return JsonConfigReturnStatus::OK;\n}\n\nJsonConfigReturnStatus JsonConfigurator::load_cv_gate()\n{\n    auto [status, cv_config] = _parse_section(JsonSection::CV_GATE);\n    if (status != JsonConfigReturnStatus::OK)\n    {\n        return status;\n    }\n\n    if (cv_config.HasMember(\"cv_inputs\"))\n    {\n        for (const auto& cv_in : cv_config[\"cv_inputs\"].GetArray())\n        {\n            auto res = _engine->connect_cv_to_parameter(cv_in[\"processor\"].GetString(),\n                                                        cv_in[\"parameter\"].GetString(),\n                                                        cv_in[\"cv\"].GetInt());\n            if (res != EngineReturnStatus::OK)\n            {\n                ELKLOG_LOG_ERROR(\"Failed to connect cv input {} to parameter {} on processor {}\",\n                               cv_in[\"cv\"].GetInt(),\n                               cv_in[\"parameter\"].GetString(),\n                               cv_in[\"processor\"].GetString());\n            }\n        }\n    }\n\n    if (cv_config.HasMember(\"cv_outputs\"))\n    {\n        for (const auto& cv_out : cv_config[\"cv_outputs\"].GetArray())\n        {\n            auto res = _engine->connect_cv_from_parameter(cv_out[\"processor\"].GetString(),\n                                                          cv_out[\"parameter\"].GetString(),\n                                                          cv_out[\"cv\"].GetInt());\n            if (res != EngineReturnStatus::OK)\n            {\n                ELKLOG_LOG_ERROR(\"Failed to connect cv output {} to parameter {} on processor {}\",\n                               cv_out[\"cv\"].GetInt(),\n                               cv_out[\"parameter\"].GetString(),\n                               cv_out[\"processor\"].GetString());\n            }\n        }\n    }\n\n    if (cv_config.HasMember(\"gate_inputs\"))\n    {\n        for (const auto& gate_in : cv_config[\"gate_inputs\"].GetArray())\n        {\n            if (gate_in[\"mode\"] == \"sync\")\n            {\n                auto res = _engine->connect_gate_to_sync(gate_in[\"gate\"].GetInt(),\n                                                         gate_in[\"ppq_ticks\"].GetInt());\n                if (res != EngineReturnStatus::OK)\n                {\n                    ELKLOG_LOG_ERROR(\"Failed to set gate {} as sync input\", gate_in[\"gate\"].GetInt());\n                }\n\n            }\n            else if (gate_in[\"mode\"] == \"note_event\")\n            {\n                auto res = _engine->connect_gate_to_processor(gate_in[\"processor\"].GetString(),\n                                                              gate_in[\"gate\"].GetInt(),\n                                                              gate_in[\"note_no\"].GetInt(),\n                                                              gate_in[\"channel\"].GetInt());\n                if (res != EngineReturnStatus::OK)\n                {\n                    ELKLOG_LOG_ERROR(\"Failed to connect gate {} to processor {}\",\n                                   gate_in[\"gate\"].GetInt(),\n                                   gate_in[\"processor\"].GetString());\n                }\n            }\n        }\n    }\n\n    if (cv_config.HasMember(\"gate_outputs\"))\n    {\n        for (const auto& gate_out : cv_config[\"gate_outputs\"].GetArray())\n        {\n            if (gate_out[\"mode\"] == \"sync\")\n            {\n                auto res = _engine->connect_sync_to_gate(gate_out[\"gate\"].GetInt(),\n                                                         gate_out[\"ppq_ticks\"].GetInt());\n                if (res != EngineReturnStatus::OK)\n                {\n                    ELKLOG_LOG_ERROR(\"Failed to set gate {} as sync output\", gate_out[\"gate\"].GetInt());\n                }\n\n            }\n            else if (gate_out[\"mode\"] == \"note_event\")\n            {\n                auto res = _engine->connect_gate_from_processor(gate_out[\"processor\"].GetString(),\n                                                                gate_out[\"gate\"].GetInt(),\n                                                                gate_out[\"note_no\"].GetInt(),\n                                                                gate_out[\"channel\"].GetInt());\n                if (res != EngineReturnStatus::OK)\n                {\n                    ELKLOG_LOG_ERROR(\"Failed to connect gate {} from processor {}\",\n                                   gate_out[\"gate\"].GetInt(),\n                                   gate_out[\"processor\"].GetString());\n                }\n            }\n        }\n    }\n\n    return JsonConfigReturnStatus::OK;\n}\n\nJsonConfigReturnStatus JsonConfigurator::load_events()\n{\n    auto [status, events] = _parse_section(JsonSection::EVENTS);\n    if (status != JsonConfigReturnStatus::OK)\n    {\n        return status;\n    }\n\n    auto dispatcher = _engine->event_dispatcher();\n    for (auto& json_event : events.GetArray())\n    {\n        if (auto e = _parse_event(json_event, IGNORE_TIMESTAMP); e != nullptr)\n        {\n            dispatcher->post_event(std::move(e));\n        }\n    }\n\n    return JsonConfigReturnStatus::OK;\n}\n\nstd::pair<JsonConfigReturnStatus, std::vector<std::unique_ptr<Event>>> JsonConfigurator::load_event_list()\n{\n    std::vector<std::unique_ptr<Event>> events;\n    auto [status, json_events] = _parse_section(JsonSection::EVENTS);\n    if (status != JsonConfigReturnStatus::OK)\n    {\n        return std::make_pair(status, std::move(events));\n    }\n\n    for (auto& json_event : json_events.GetArray())\n    {\n        if (auto e = _parse_event(json_event, USE_TIMESTAMP); e != nullptr)\n        {\n            events.push_back(std::move(e));\n        }\n    }\n\n    return std::make_pair(JsonConfigReturnStatus::OK, std::move(events));\n}\n\nJsonConfigReturnStatus JsonConfigurator::load_initial_state()\n{\n    ELKLOG_LOG_DEBUG(\"Loading initial processor state\");\n\n    auto [status, json_states] = _parse_section(JsonSection::STATE);\n    if (status != JsonConfigReturnStatus::OK)\n    {\n        return status;\n    }\n\n    for (auto& json_state : json_states.GetArray())\n    {\n        ProcessorState state;\n        auto processor = _processor_container->mutable_processor(json_state[\"processor\"].GetString());\n        if (!processor)\n        {\n            ELKLOG_LOG_WARNING(\"Invalid processor name: \\\"{}\\\"\", json_state[\"processor\"].GetString());\n            return JsonConfigReturnStatus::INVALID_PLUGIN_NAME;\n        }\n\n        if (json_state.HasMember(\"bypassed\"))\n        {\n            state.set_bypass(json_state[\"bypassed\"].GetBool());\n        }\n\n        if (json_state.HasMember(\"program\"))\n        {\n            state.set_program(json_state[\"program\"].GetInt());\n        }\n\n        if (json_state.HasMember(\"parameters\"))\n        {\n            for (const auto& parameter : json_state[\"parameters\"].GetObject())\n            {\n                auto param = processor->parameter_from_name(parameter.name.GetString());\n                if (!param)\n                {\n                    ELKLOG_LOG_WARNING(\"Invalid parameter name: \\\"{}\\\"\", parameter.name.GetString());\n                    return JsonConfigReturnStatus::INVALID_PARAMETER;\n                }\n                state.add_parameter_change(param->id(), parameter.value.GetFloat());\n            }\n        }\n\n        if (json_state.HasMember(\"properties\"))\n        {\n            for (const auto& property : json_state[\"properties\"].GetObject())\n            {\n                auto param = processor->parameter_from_name(property.name.GetString());\n                if (!param)\n                {\n                    ELKLOG_LOG_WARNING(\"Invalid property name: \\\"{}\\\"\", property.name.GetString());\n                    return JsonConfigReturnStatus::INVALID_CONFIGURATION;\n                }\n\n                state.add_property_change(param->id(), property.value.GetString());\n            }\n        }\n\n        processor->set_state(&state, false);\n    }\n\n    return JsonConfigReturnStatus::OK;\n}\n\nvoid JsonConfigurator::set_osc_frontend(control_frontend::OSCFrontend* osc_frontend)\n{\n    _osc_frontend = osc_frontend;\n}\n\nstd::pair<JsonConfigReturnStatus, const rapidjson::Value&> JsonConfigurator::_parse_section(JsonSection section)\n{\n    if (!_json_data.IsObject())\n    {\n        auto res = _load_data(_json_string);\n        if (res != JsonConfigReturnStatus::OK)\n        {\n            return {res, _json_data};\n        }\n    }\n\n    if (!_validate_against_schema(_json_data, section))\n    {\n        ELKLOG_LOG_ERROR(\"Config file {} does not follow schema: {}\", _json_string, (int)section);\n        return {JsonConfigReturnStatus::INVALID_CONFIGURATION, _json_data};\n    }\n\n    auto name = section_name(section);\n    if (!_json_data.HasMember(name))\n    {\n        ELKLOG_LOG_INFO(\"Config file does not have any \\\"{}\\\" definitions\", name);\n        return {JsonConfigReturnStatus::NOT_DEFINED, _json_data};\n    }\n\n    return {JsonConfigReturnStatus::OK, _json_data[name]};\n}\n\nJsonConfigReturnStatus JsonConfigurator::_make_track(const rapidjson::Value& track_def, engine::TrackType type)\n{\n    auto name = track_def[\"name\"].GetString();\n    EngineReturnStatus status = EngineReturnStatus::ERROR;\n    ObjectId track_id {0};\n\n    switch (type)\n    {\n        case TrackType::REGULAR:\n        {\n            std::optional<int> thread = std::nullopt;\n            if (track_def.HasMember(\"thread\") && track_def[\"thread\"].IsInt())\n            {\n                thread = track_def[\"thread\"].GetInt();\n            }\n            if (track_def.HasMember(\"multibus\") && track_def[\"multibus\"].GetBool())\n            {\n                std::tie(status, track_id) = _engine->create_multibus_track(name, track_def[\"buses\"].GetInt(), thread);\n            }\n            else if (track_def.HasMember(\"channels\"))\n            {\n                std::tie(status, track_id) = _engine->create_track(name, track_def[\"channels\"].GetInt(), thread);\n            }\n            break;\n        }\n        case TrackType::PRE:\n        {\n            std::tie(status, track_id) = _engine->create_pre_track(name);\n            break;\n        }\n        case TrackType::POST:\n        {\n            std::tie(status, track_id) = _engine->create_post_track(name);\n            break;\n        }\n    }\n\n    if (status == EngineReturnStatus::INVALID_PLUGIN || status == EngineReturnStatus::INVALID_PROCESSOR)\n    {\n        ELKLOG_LOG_ERROR(\"Track {} in JSON config file duplicate or invalid name\", name);\n        return JsonConfigReturnStatus::INVALID_TRACK_NAME;\n    }\n\n    if (status != EngineReturnStatus::OK)\n    {\n        ELKLOG_LOG_ERROR(\"Track Name {} failed to create\", name);\n        return JsonConfigReturnStatus::INVALID_CONFIGURATION;\n    }\n\n    ELKLOG_LOG_DEBUG(\"Successfully added track \\\"{}\\\" to the engine\", name);\n    if (type == TrackType::REGULAR)\n    {\n        auto connect_status = _connect_audio_to_track(track_def, name, track_id);\n        if (connect_status != JsonConfigReturnStatus::OK)\n        {\n            return connect_status;\n        }\n\n        if (status != EngineReturnStatus::OK)\n        {\n            ELKLOG_LOG_ERROR(\"Error connecting track \\\"{}\\\" to output bus, error {}\", name, static_cast<int>(status));\n            return JsonConfigReturnStatus::INVALID_CONFIGURATION;\n        }\n    }\n\n    for (const auto& def : track_def[\"plugins\"].GetArray())\n    {\n        auto plugin_status = _add_plugin(def, name, track_id);\n        if (plugin_status != JsonConfigReturnStatus::OK)\n        {\n            return plugin_status;\n        }\n    }\n\n    ELKLOG_LOG_DEBUG(\"Successfully added {} Track {} to the engine\", type == TrackType::REGULAR? \"\" :\"Master\", name);\n    return JsonConfigReturnStatus::OK;\n}\n\nint JsonConfigurator::_get_midi_channel(const rapidjson::Value& channels) const\n{\n    if (channels.IsString())\n    {\n        return midi::MidiChannel::OMNI;\n    }\n\n    return channels.GetInt();\n}\n\nstd::unique_ptr<Event> JsonConfigurator::_parse_event(const rapidjson::Value& json_event, bool with_timestamp)\n{\n    Time timestamp = with_timestamp ?\n                     std::chrono::microseconds(static_cast<int>(std::round(json_event[\"time\"].GetDouble() * 1'000'000))) :\n                     IMMEDIATE_PROCESS;\n\n    const rapidjson::Value& data = json_event[\"data\"];\n    auto processor = _engine->processor_container()->processor(data[\"plugin_name\"].GetString());\n    if (processor == nullptr)\n    {\n        ELKLOG_LOG_WARNING(\"Unrecognised plugin: \\\"{}\\\"\", data[\"plugin_name\"].GetString());\n        return nullptr;\n    }\n\n    if (json_event[\"type\"] == \"parameter_change\")\n    {\n        auto parameter = processor->parameter_from_name(data[\"parameter_name\"].GetString());\n        if (parameter == nullptr)\n        {\n            ELKLOG_LOG_WARNING(\"Unrecognised parameter: {}\", data[\"parameter_name\"].GetString());\n            return nullptr;\n        }\n\n        return std::make_unique<ParameterChangeEvent>(ParameterChangeEvent::Subtype::FLOAT_PARAMETER_CHANGE,\n                                                      processor->id(),\n                                                      parameter->id(),\n                                                      data[\"value\"].GetFloat(),\n                                                      timestamp);\n    }\n\n    if (json_event[\"type\"] == \"property_change\")\n    {\n        auto parameter = processor->parameter_from_name(data[\"property_name\"].GetString());\n        if (parameter == nullptr)\n        {\n            ELKLOG_LOG_WARNING(\"Unrecognised property: {}\", data[\"property_name\"].GetString());\n            return nullptr;\n        }\n\n        return std::make_unique<PropertyChangeEvent>(processor->id(),\n                                                     parameter->id(),\n                                                     data[\"value\"].GetString(),\n                                                     timestamp);\n    }\n\n    if (json_event[\"type\"] == \"note_on\")\n    {\n        return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::NOTE_ON,\n                                               processor->id(),\n                                               0, // channel\n                                               data[\"note\"].GetUint(),\n                                               data[\"velocity\"].GetFloat(),\n                                               timestamp);\n    }\n\n    if (json_event[\"type\"] == \"note_off\")\n    {\n        return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::NOTE_OFF,\n                                               processor->id(),\n                                               0, // channel\n                                               data[\"note\"].GetUint(),\n                                               data[\"velocity\"].GetFloat(),\n                                               timestamp);\n    }\n\n    return nullptr;\n}\n\nbool JsonConfigurator::_validate_against_schema(rapidjson::Value& config, JsonSection section)\n{\n    const char* schema_char_array = section_schema(section);\n    assert(schema_char_array);\n\n    rapidjson::Document schema;\n    schema.Parse(schema_char_array);\n    assert(schema.HasParseError() == false);\n    rapidjson::SchemaDocument schema_document(schema);\n    rapidjson::SchemaValidator schema_validator(schema_document);\n\n    // Validate Schema\n    if (!config.Accept(schema_validator))\n    {\n        rapidjson::Pointer invalid_config_pointer = schema_validator.GetInvalidDocumentPointer();\n        rapidjson::StringBuffer string_buffer;\n        invalid_config_pointer.Stringify(string_buffer);\n        std::string error_node = string_buffer.GetString();\n\n        if (!error_node.empty())\n        {\n            ELKLOG_LOG_ERROR(\"Schema validation failure at {}\", error_node);\n        }\n\n        return false;\n    }\n\n    return true;\n}\n\nJsonConfigReturnStatus JsonConfigurator::_load_data(const std::string& data)\n{\n    _json_data.Parse(data.c_str());\n\n    if (_json_data.HasParseError())\n    {\n        [[maybe_unused]] int err_offset = static_cast<int>(_json_data.GetErrorOffset());\n        ELKLOG_LOG_ERROR(\"Error parsing JSON config file: {} @ pos {}: \\\"{}\\\"\",\n                       rapidjson::GetParseError_En(_json_data.GetParseError()),\n                       err_offset,\n                       data.substr(std::max(0, err_offset - ERROR_DISPLAY_CHARS),\n                                   ERROR_DISPLAY_CHARS));\n\n        return JsonConfigReturnStatus::INVALID_FILE;\n    }\n\n    return JsonConfigReturnStatus::OK;\n}\n\nJsonConfigReturnStatus JsonConfigurator::_connect_audio_to_track(const rapidjson::Value& track_def,\n                                                                 [[maybe_unused]] const std::string& track_name,\n                                                                 ObjectId track_id)\n{\n    EngineReturnStatus status;\n    for (const auto& con : track_def[\"inputs\"].GetArray())\n    {\n        if (con.HasMember(\"engine_bus\"))\n        {\n            status = _engine->connect_audio_input_bus(con[\"engine_bus\"].GetInt(),\n                                                      con[\"track_bus\"].GetInt(),\n                                                      track_id);\n        }\n        else\n        {\n            status = _engine->connect_audio_input_channel(con[\"engine_channel\"].GetInt(),\n                                                          con[\"track_channel\"].GetInt(),\n                                                          track_id);\n        }\n\n        if (status != EngineReturnStatus::OK)\n        {\n            ELKLOG_LOG_ERROR(\"Error connecting input bus to track \\\"{}\\\", error {}\", track_name, static_cast<int>(status));\n            return JsonConfigReturnStatus::INVALID_CONFIGURATION;\n        }\n    }\n\n    for (const auto& con : track_def[\"outputs\"].GetArray())\n    {\n        if (con.HasMember(\"engine_bus\"))\n        {\n            status = _engine->connect_audio_output_bus(con[\"engine_bus\"].GetInt(),\n                                                       con[\"track_bus\"].GetInt(),\n                                                       track_id);\n        }\n        else\n        {\n            status = _engine->connect_audio_output_channel(con[\"engine_channel\"].GetInt(),\n                                                           con[\"track_channel\"].GetInt(),\n                                                           track_id);\n\n        }\n\n        if (status != EngineReturnStatus::OK)\n        {\n            ELKLOG_LOG_ERROR(\"Error connection track \\\"{}\\\" to output bus, error {}\", track_name, static_cast<int>(status));\n            return JsonConfigReturnStatus::INVALID_CONFIGURATION;\n        }\n    }\n\n    return JsonConfigReturnStatus::OK;\n}\n\nJsonConfigReturnStatus JsonConfigurator::_add_plugin(const rapidjson::Value& plugin_def,\n                                                     [[maybe_unused]] const std::string& track_name,\n                                                     ObjectId track_id)\n{\n    std::string plugin_uid;\n    std::string plugin_path;\n    std::string plugin_name = plugin_def[\"name\"].GetString();\n    PluginType plugin_type;\n    std::string type = plugin_def[\"type\"].GetString();\n\n    if (type == \"internal\")\n    {\n        plugin_type = PluginType::INTERNAL;\n        plugin_uid = plugin_def[\"uid\"].GetString();\n    }\n    else if (type == \"vst2x\")\n    {\n        plugin_type = PluginType::VST2X;\n        plugin_path = plugin_def[\"path\"].GetString();\n    }\n    else if (type == \"vst3x\")\n    {\n        plugin_uid = plugin_def[\"uid\"].GetString();\n        plugin_path = plugin_def[\"path\"].GetString();\n        plugin_type = PluginType::VST3X;\n    }\n    else // Anything else should have been caught by the validation step before this\n    {\n        plugin_type = PluginType::LV2;\n        plugin_path = plugin_def[\"uri\"].GetString();\n    }\n\n    PluginInfo plugin_info;\n    plugin_info.uid = plugin_uid;\n    plugin_info.path = plugin_path;\n    plugin_info.type = plugin_type;\n\n    auto [status, plugin_id] = _engine->create_processor(plugin_info, plugin_name);\n    if (status != EngineReturnStatus::OK)\n    {\n        if (status == EngineReturnStatus::INVALID_PLUGIN_UID)\n        {\n            ELKLOG_LOG_ERROR(\"Invalid plugin uid {} in JSON config file\", plugin_uid);\n            return JsonConfigReturnStatus::INVALID_PLUGIN_PATH;\n        }\n        return JsonConfigReturnStatus::INVALID_CONFIGURATION;\n    }\n\n    status = _engine->add_plugin_to_track(plugin_id, track_id);\n    if (status != EngineReturnStatus::OK)\n    {\n        return JsonConfigReturnStatus::INVALID_CONFIGURATION;\n    }\n\n    ELKLOG_LOG_DEBUG(\"Successfully added Plugin \\\"{}\\\" to track: \\\"{}\\\"\", plugin_name, track_name);\n\n    return JsonConfigReturnStatus::OK;\n}\n\n} // end namespace sushi::internal::jsonconfig\n"
  },
  {
    "path": "src/engine/json_configurator.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n * @brief Class to configure the audio engine using Json config files.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n *\n * @details This file contains class JsonConfigurator which provides public methods\n * to read configuration data from Json config files, validate its schema and configure\n * the engine with it. It can load tracks and midi connections from the config file.\n */\n\n#ifndef SUSHI_CONFIG_FROM_JSON_H\n#define SUSHI_CONFIG_FROM_JSON_H\n\n#include <optional>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_TYPE_LIMITS\n#include \"rapidjson/document.h\"\nELK_POP_WARNING\n\n#include \"base_engine.h\"\n#include \"engine/midi_dispatcher.h\"\n#include \"control_frontends/osc_frontend.h\"\n\nnamespace sushi::internal::jsonconfig {\n\nenum class JsonConfigReturnStatus\n{\n    OK,\n    INVALID_CONFIGURATION,\n    INVALID_TRACK_NAME,\n    INVALID_PLUGIN_PATH,\n    INVALID_PARAMETER,\n    INVALID_PLUGIN_NAME,\n    INVALID_MIDI_PORT,\n    INVALID_FILE,\n    NOT_DEFINED\n};\n\nenum class JsonSection\n{\n    HOST_CONFIG,\n    TRACKS,\n    PRE_TRACK,\n    POST_TRACK,\n    MIDI,\n    OSC,\n    CV_GATE,\n    EVENTS,\n    STATE\n};\n\nstruct ControlConfig\n{\n    std::optional<int> cv_inputs;\n    std::optional<int> cv_outputs;\n    std::optional<int> midi_inputs;\n    std::optional<int> midi_outputs;\n    std::vector<std::tuple<int, int, bool>> rt_midi_input_mappings;\n    std::vector<std::tuple<int, int, bool>> rt_midi_output_mappings;\n};\n\nclass Accessor;\n\nclass JsonConfigurator\n{\npublic:\n    JsonConfigurator(engine::BaseEngine* engine,\n                     midi_dispatcher::MidiDispatcher* midi_dispatcher,\n                     const engine::BaseProcessorContainer* processor_container,\n                     const std::string& json_string) : _engine(engine),\n                                                       _midi_dispatcher(midi_dispatcher),\n                                                       _processor_container(processor_container),\n                                                       _json_string(json_string) {}\n\n    ~JsonConfigurator() = default;\n\n    /**\n     * @brief Reads the json config, and returns all control frontend configuration options\n     *        that are not set on the audio engine directly.\n     * @return A tuple of status and AudioConfig struct, ControlConfig is only valid if status is\n     *         JsonConfigReturnStatus::OK\n     */\n    std::pair<JsonConfigReturnStatus, ControlConfig> load_control_config();\n\n    /**\n     * @brief Reads the json config, and set the given host configuration options\n     * @param path_to_file String which denotes the path of the file.\n     * @return JsonConfigReturnStatus::OK if success, different error code otherwise.\n     */\n    JsonConfigReturnStatus load_host_config();\n\n    /**\n     * @brief Reads the json config, searches for valid track definitions and configures\n     *        the engine with the specified tracks and master tracks\n     * @return JsonConfigReturnStatus::OK if success, different error code otherwise.\n     */\n    JsonConfigReturnStatus load_tracks();\n\n    /**\n     * @brief Reads the json config, searches for valid MIDI connections and\n     *        MIDI CC Mapping definitions and configures the engine with the specified MIDI information.\n     * @return JsonConfigReturnStatus::OK if success, different error code otherwise.\n     */\n    JsonConfigReturnStatus load_midi();\n\n    /**\n     * @brief Reads the json config, searches for valid OSC echo definitions and\n     * configures the engine with the specified information.\n     * @return JsonConfigReturnStatus::OK if success, different error code otherwise.\n     */\n    JsonConfigReturnStatus load_osc();\n\n    /**\n     * @brief Reads the json config, searches for valid control voltage and gate\n     *        connection definitions and configures the engine with the specified routing.\n     * @return JsonConfigReturnStatus::OK if success, different error code otherwise.\n     */\n    JsonConfigReturnStatus load_cv_gate();\n\n    /**\n     * @brief Reads the json config, searches for a valid \"events\" definition and\n     *        queues them to the engines internal queue.\n     * @return JsonConfigReturnStatus::OK if success, different error code otherwise.\n     */\n    JsonConfigReturnStatus load_events();\n\n    /**\n     * @brief Reads the json config, searches for a valid \"events\" definition and\n     *        returns all parsed events as a list.\n     * @return An std::vector with the parsed events which is only valid if the status\n     *         returned is JsonConfigReturnStatus::OK\n     */\n    std::pair<JsonConfigReturnStatus, std::vector<std::unique_ptr<Event>>> load_event_list();\n\n    /**\n     * @brief Reads the json config, searches for a valid \"initial_state\" definition\n     *        and configures the processors with the values specified\n     * @return JsonConfigReturnStatus::OK if success, different error code otherwise.\n     */\n    JsonConfigReturnStatus load_initial_state();\n\n    void set_osc_frontend(control_frontend::OSCFrontend* osc_frontend);\n\nprivate:\n    friend Accessor;\n\n    /**\n     * @brief Helper function to retrieve a particular section of the json configuration\n     * @param section JsonSection to denote which section is to be validated.\n     * @return JsonConfigReturnStatus::OK if success, different error code otherwise.\n     */\n    std::pair<JsonConfigReturnStatus, const rapidjson::Value&> _parse_section(JsonSection section);\n\n    /**\n     * @brief Uses Engine's API to create a single track with the specified number of channels and adds\n     *        the respective plugins to the track if they are defined in the file. Used by load_tracks.\n     * @param track_def rapidjson document object representing a single track and its details.\n     * @return JsonConfigReturnStatus::OK if success, different error code otherwise.\n     */\n    JsonConfigReturnStatus _make_track(const rapidjson::Value& track_def, engine::TrackType type);\n\n    JsonConfigReturnStatus _connect_audio_to_track(const rapidjson::Value& track_def, const std::string& track_name, ObjectId track_id);\n\n    JsonConfigReturnStatus _add_plugin(const rapidjson::Value& plugin_def, const std::string& track_name, ObjectId track_id);\n\n    /**\n     * @brief Helper function to extract the number of midi channels in the midi definition.\n     * @param channels rapidjson document object containing the channel information parsed from the file.\n     * @return The number of MIDI channels.\n     */\n    [[nodiscard]] int _get_midi_channel(const rapidjson::Value& channels) const;\n\n    /* Helper enum for more expressive code */\n    enum EventParseMode : bool\n    {\n        IGNORE_TIMESTAMP = false,\n        USE_TIMESTAMP = true,\n    };\n    /**\n     * @brief Helper function to parse a single event\n     * @param json_event A json value representing an event\n     * @param with_timestamp If set to true, the timestamp from the json definition will be used\n     *        if set to false, the event timestamp will be set for immediate processing\n     * @return A pointer to an Event if successful, nullptr otherwise\n     */\n    std::unique_ptr<Event> _parse_event(const rapidjson::Value& json_event, bool with_timestamp);\n\n    /**\n     * @brief function which validates the json data against the respective schema.\n     * @param config rapidjson document object containing the json data parsed from the file\n     * @param section JsonSection to denote which json section is to be validated.\n     * @return true if json follows schema, false otherwise\n     */\n    static bool _validate_against_schema(rapidjson::Value& config, JsonSection section);\n\n    JsonConfigReturnStatus _load_data(const std::string& data);\n\n    engine::BaseEngine* _engine;\n    midi_dispatcher::MidiDispatcher* _midi_dispatcher;\n    control_frontend::OSCFrontend* _osc_frontend {nullptr};\n    const engine::BaseProcessorContainer* _processor_container;\n\n    std::string _json_string;\n    rapidjson::Document _json_data;\n};\n\n} // end namespace sushi::internal::jsonconfig\n\n#endif // SUSHI_CONFIG_FROM_JSON_H\n"
  },
  {
    "path": "src/engine/json_schemas/cv_gate_schema.json",
    "content": "R\"(\n{\n  \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n  \"title\": \"Sushi MIDI JSON Schema\",\n  \"description\": \"JSON Schema to validate MIDI definitions\",\n\n  \"type\": \"object\",\n  \"properties\":\n  {\n    \"cv_control\":\n    {\n      \"type\": \"object\",\n      \"properties\":\n      {\n        \"cv_inputs\":\n        {\n          \"type\":\"array\",\n          \"items\":\n          {\n            \"type\": \"object\",\n            \"properties\":\n            {\n              \"cv\":\n              {\n                \"type\": \"integer\",\n                \"minimum\": 0\n              },\n              \"processor\":\n              {\n                \"type\": \"string\",\n                \"minLength\": 1\n              },\n              \"parameter\":\n              {\n                \"type\": \"string\",\n                \"minLength\": 1\n              }\n            },\n            \"required\": [\"cv\", \"processor\", \"parameter\"]\n          }\n        },\n        \"cv_outputs\":\n        {\n          \"type\":\"array\",\n          \"items\":\n          {\n            \"type\": \"object\",\n            \"properties\":\n            {\n              \"cv\":\n              {\n                \"type\": \"integer\",\n                \"minimum\": 0\n              },\n              \"processor\":\n              {\n                \"type\": \"string\",\n                \"minLength\": 1\n              },\n              \"parameter\":\n              {\n                \"type\": \"string\",\n                \"minLength\": 1\n              }\n            },\n            \"required\": [\"cv\", \"processor\", \"parameter\"]\n          }\n        },\n        \"gate_inputs\":\n        {\n          \"type\":\"array\",\n          \"items\":\n          {\n            \"oneOf\":\n            [\n              {\n                \"type\": \"object\",\n                \"properties\":\n                {\n                  \"gate\":\n                  {\n                    \"type\": \"integer\",\n                    \"minimum\": 0\n                  },\n                  \"mode\":\n                  {\n                    \"enum\": [\"note_event\"]\n                  },\n                  \"note_no\" :\n                  {\n                    \"type\": \"integer\",\n                    \"minimum\": 0,\n                    \"maximum\": 127\n                  },\n                  \"channel\" :\n                  {\n                    \"type\" : \"integer\",\n                    \"minimum\": 0,\n                    \"maximum\": 15\n                  },\n                  \"processor\" :\n                  {\n                    \"type\": \"string\",\n                    \"minLength\" : 1\n                  }\n                },\n                \"required\": [\"gate\", \"mode\", \"processor\", \"note_no\", \"channel\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\":\n                {\n                  \"gate\":\n                  {\n                    \"type\": \"integer\",\n                    \"minimum\": 0\n                  },\n                  \"mode\":\n                  {\n                    \"enum\": [\"sync\"]\n                  },\n                  \"ppq_ticks\" :\n                  {\n                    \"type\": \"integer\",\n                    \"minimum\": 1,\n                    \"maximum\": 48\n                  }\n                },\n                \"required\": [\"gate\", \"mode\", \"ppq_ticks\"]\n              }\n            ]\n          }\n        },\n        \"gate_outputs\":\n        {\n          \"type\":\"array\",\n          \"items\":\n          {\n            \"oneOf\":\n            [\n              {\n                \"type\": \"object\",\n                \"properties\":\n                {\n                  \"gate\":\n                  {\n                    \"type\": \"integer\",\n                    \"minimum\": 0\n                  },\n                  \"mode\":\n                  {\n                    \"enum\": [\"note_event\"]\n                  },\n                  \"note_no\" :\n                  {\n                    \"type\": \"integer\",\n                    \"minimum\": 0,\n                    \"maximum\": 127\n                  },\n                  \"channel\" :\n                  {\n                    \"type\" : \"integer\",\n                    \"minimum\": 0,\n                    \"maximum\": 15\n                  },\n                  \"processor\" :\n                  {\n                    \"type\": \"string\",\n                    \"minLength\" : 1\n                  }\n                },\n                \"required\": [\"gate\", \"mode\", \"processor\", \"note_no\", \"channel\"]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\":\n                {\n                  \"gate\":\n                  {\n                    \"type\": \"integer\",\n                    \"minimum\": 0\n                  },\n                  \"mode\":\n                  {\n                    \"enum\": [\"sync\"]\n                  },\n                  \"ppq_ticks\" :\n                  {\n                    \"type\": \"integer\",\n                    \"minimum\": 1,\n                    \"maximum\": 48\n                  }\n                },\n                \"required\": [\"gate\", \"mode\", \"ppq_ticks\"]\n              }\n            ]\n          }\n        }\n      },\n      \"additionalProperties\": false\n    }\n  }\n}\n)\""
  },
  {
    "path": "src/engine/json_schemas/events_schema.json",
    "content": "R\"(\n{\n  \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n  \"title\": \"Sushi Events JSON Schema\",\n  \"description\": \"JSON Schema to validate events definitions\",\n\n  \"type\": \"object\",\n  \"properties\":\n  {\n    \"events\":\n    {\n      \"type\": \"array\",\n      \"items\":\n      {\n        \"type\": \"object\",\n        \"properties\":\n        {\n          \"time\":\n          {\n            \"type\": \"number\"\n          },\n          \"type\":\n          {\n            \"enum\": [\"parameter_change\", \"property_change\", \"note_on\", \"note_off\"]\n          },\n          \"data\":\n          {\n            \"type\": \"object\",\n            \"oneOf\":\n            [\n              {\n                \"properties\":\n                {\n                  \"plugin_name\":\n                  {\n                    \"type\": \"string\"\n                  },\n                  \"parameter_name\":\n                  {\n                    \"type\": \"string\"\n                  },\n                  \"value\":\n                  {\n                    \"type\": \"number\"\n                  }\n                },\n                \"required\": [\"plugin_name\", \"parameter_name\", \"value\"]\n              },\n              {\n                \"properties\":\n                {\n                  \"plugin_name\":\n                  {\n                    \"type\": \"string\"\n                  },\n                  \"property_name\":\n                  {\n                    \"type\": \"string\"\n                  },\n                  \"value\":\n                  {\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\"plugin_name\", \"property_name\", \"value\"]\n              },\n              {\n                \"properties\":\n                {\n                  \"plugin_name\":\n                  {\n                    \"type\": \"string\"\n                  },\n                  \"note\":\n                  {\n                    \"type\": \"integer\"\n                  },\n                  \"velocity\":\n                  {\n                    \"type\": \"number\"\n                  }\n                },\n                \"required\": [\"plugin_name\", \"note\", \"velocity\"]\n              }\n            ]\n          }\n        },\n        \"required\": [\"time\", \"type\", \"data\"],\n        \"additionalProperties\": false\n      }\n    }\n  }\n}\n)\""
  },
  {
    "path": "src/engine/json_schemas/host_config_schema.json",
    "content": "R\"(\n{\n  \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n  \"title\": \"Sushi Host Config JSON Schema\",\n  \"description\": \"JSON Schema to validate host configuration definitions\",\n\n  \"type\": \"object\",\n  \"properties\":\n  {\n    \"host_config\":\n    {\n      \"type\": \"object\",\n      \"properties\":\n      {\n        \"samplerate\":\n        {\n          \"type\": \"integer\",\n          \"minimum\": 1000,\n          \"maximum\": 192000\n        },\n        \"tempo\":\n        {\n          \"type\" : \"number\",\n          \"minimum\": 1\n        },\n        \"time_signature\" :\n        {\n          \"type\": \"object\",\n          \"properties\":\n          {\n            \"numerator\":\n            {\n              \"type\": \"integer\"\n            },\n            \"denominator\":\n            {\n              \"type\": \"integer\"\n            }\n          },\n          \"required\": [\"numerator\", \"denominator\"]\n        },\n        \"playing_mode\":\n        {\n          \"enum\": [\"stopped\", \"playing\"]\n        },\n        \"tempo_sync\":\n        {\n          \"enum\": [\"internal\", \"midi\", \"ableton_link\"]\n        },\n        \"audio_clip_detection\" :\n        {\n          \"type\": \"object\",\n          \"properties\":\n          {\n            \"inputs\":\n            {\n              \"type\": \"boolean\"\n            },\n            \"outputs\":\n            {\n              \"type\": \"boolean\"\n            }\n          }\n        },\n        \"master_limiter\" :\n        {\n          \"type\": \"boolean\"\n        },\n        \"cv_inputs\":\n        {\n          \"type\": \"integer\",\n          \"minimum\": 0,\n          \"maximum\": 4\n        },\n        \"cv_outputs\":\n        {\n          \"type\": \"integer\",\n          \"minimum\": 0,\n          \"maximum\": 4\n        },\n        \"midi_inputs\":\n        {\n          \"type\": \"integer\",\n          \"minimum\": 0\n        },\n        \"midi_outputs\":\n        {\n          \"type\": \"integer\",\n          \"minimum\": 0\n        },\n        \"rt_midi_input_mappings\":\n        {\n          \"type\": \"array\",\n          \"items\":\n          {\n            \"type\": \"object\",\n            \"properties\":\n            {\n              \"rt_midi_device\":\n              {\n                \"type\": \"integer\"\n              },\n              \"sushi_midi_port\":\n              {\n                \"type\": \"integer\"\n              },\n              \"virtual_port\":\n              {\n                \"type\": \"boolean\"\n              }\n            }\n          },\n          \"required\": [\"rt_midi_device\", \"sushi_midi_port\"]\n        },\n        \"rt_midi_output_mappings\":\n        {\n          \"type\": \"array\",\n          \"items\":\n          {\n            \"type\": \"object\",\n            \"properties\":\n            {\n              \"rt_midi_device\":\n              {\n                \"type\": \"integer\"\n              },\n              \"sushi_midi_port\":\n              {\n                \"type\": \"integer\"\n              },\n              \"virtual_port\":\n              {\n                \"type\": \"boolean\"\n              }\n            }\n          },\n          \"required\": [\"rt_midi_device\", \"sushi_midi_port\"]\n        }\n      },\n      \"required\": [\"samplerate\"],\n      \"additionalProperties\": false\n    }\n  },\n  \"required\": [\"host_config\"]\n}\n)\"\n"
  },
  {
    "path": "src/engine/json_schemas/midi_schema.json",
    "content": "R\"(\n{\n  \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n  \"title\": \"Sushi MIDI JSON Schema\",\n  \"description\": \"JSON Schema to validate MIDI definitions\",\n\n  \"type\": \"object\",\n  \"properties\":\n  {\n    \"midi\":\n    {\n      \"type\": \"object\",\n      \"properties\":\n      {\n        \"track_connections\":\n        {\n          \"type\":\"array\",\n          \"items\":\n          {\n            \"type\": \"object\",\n            \"properties\":\n            {\n              \"port\":\n              {\n                \"type\": \"integer\",\n                \"minimum\": 0\n              },\n              \"channel\":\n              {\n                \"anyOf\":\n                [\n                  {\n                    \"type\": \"integer\",\n                    \"minimum\": 0,\n                    \"maximum\": 15\n                  },\n                  {\n                    \"enum\": [\"all\"]\n                  }\n                ]\n              },\n              \"track\":\n              {\n                \"type\": \"string\",\n                \"minLength\": 1\n              },\n              \"raw_midi\":\n              {\n                \"type\": \"boolean\"\n              }\n            },\n            \"required\": [\"port\", \"channel\", \"track\", \"raw_midi\"]\n          }\n        },\n        \"program_change_connections\":\n        {\n          \"type\":\"array\",\n          \"items\":\n          {\n            \"type\": \"object\",\n            \"properties\":\n            {\n              \"port\":\n              {\n                \"type\": \"integer\",\n                \"minimum\": 0\n              },\n              \"channel\":\n              {\n                \"anyOf\":\n                [\n                  {\n                    \"type\": \"integer\",\n                    \"minimum\": 0,\n                    \"maximum\": 15\n                  },\n                  {\n                    \"enum\": [\"all\"]\n                  }\n                ]\n              },\n              \"plugin\":\n              {\n                \"type\": \"string\",\n                \"minLength\": 1\n              }\n            },\n            \"required\": [\"port\", \"channel\", \"plugin\"]\n          }\n        },\n        \"cc_mappings\":\n        {\n          \"type\":\"array\",\n          \"items\":\n          {\n            \"type\": \"object\",\n            \"properties\":\n            {\n              \"port\":\n              {\n                \"type\": \"integer\",\n                \"minimum\": 0\n              },\n              \"channel\":\n              {\n                \"anyOf\":\n                [\n                  {\n                    \"type\": \"integer\",\n                    \"minimum\": 0,\n                    \"maximum\": 15\n                  },\n                  {\n                    \"enum\": [\"all\"]\n                  }\n                ]\n              },\n              \"cc_number\":\n              {\n                \"type\": \"integer\"\n              },\n              \"plugin_name\":\n              {\n                \"type\": \"string\",\n                \"minLength\": 1\n              },\n              \"parameter_name\":\n              {\n                \"type\": \"string\",\n                \"minLength\": 1\n              },\n              \"min_range\":\n              {\n                \"type\": \"number\"\n              },\n              \"max_range\":\n              {\n                \"type\": \"number\"\n              },\n              \"mode\":\n              {\n                \"enum\": [\"absolute\", \"relative\"]\n              }\n            },\n            \"required\": [\"port\", \"channel\", \"cc_number\", \"plugin_name\", \"parameter_name\", \"min_range\", \"max_range\"]\n          }\n        },\n        \"clock_output\":\n        {\n          \"type\":\"object\",\n          \"properties\":\n          {\n            \"enabled_ports\" :\n            {\n              \"type\": \"array\",\n              \"minimum\": 0,\n              \"items\":\n              {\n                \"type\": \"integer\"\n              }\n            }\n          },\n          \"required\": [\"enabled_ports\"]\n        }\n      }\n    }\n  }\n}\n)\"\n"
  },
  {
    "path": "src/engine/json_schemas/osc_schema.json",
    "content": "R\"(\n{\n  \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n  \"title\": \"Sushi OSC JSON Schema\",\n  \"description\": \"JSON Schema to validate OSC definitions\",\n\n  \"type\": \"object\",\n  \"properties\":\n  {\n    \"osc\":\n    {\n      \"type\": \"object\",\n      \"properties\" :\n      {\n        \"enable_all_processor_outputs\":\n        {\n          \"type\": \"boolean\"\n        },\n        \"enabled_processor_outputs\":\n        {\n          \"type\": \"array\",\n          \"items\":\n          {\n            \"type\": \"object\",\n            \"properties\":\n            {\n              \"processor\":\n              {\n                \"type\": \"string\",\n                \"minLength\": 1\n              },\n              \"parameter_blocklist\":\n              {\n                \"type\": \"array\",\n                \"items\":\n                {\n                  \"type\": \"object\",\n                  \"properties\":\n                  {\n                    \"parameter\":\n                    {\n                      \"type\": \"string\",\n                      \"minLength\": 1\n                    }\n                  },\n                  \"required\": [\"parameter\"]\n                }\n              }\n            },\n            \"required\": [\"processor\"]\n          }\n        }\n      },\n      \"oneOf\":\n      [\n        {\n          \"required\": [\"enable_all_processor_outputs\"]\n        },\n        {\n          \"required\" : [\"enabled_processor_outputs\"]\n        }\n      ],\n      \"additionalProperties\": false\n    }\n  }\n}\n)\"\n"
  },
  {
    "path": "src/engine/json_schemas/state_schema.json",
    "content": "R\"(\n{\n  \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n  \"title\": \"Sushi Plugin Chain JSON Schema\",\n  \"description\": \"JSON Schema to validate processor state\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"initial_state\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"processor_name\": {\n            \"type\": \"string\"\n          },\n          \"bypassed\": {\n            \"type\": \"boolean\"\n          },\n          \"program\": {\n            \"type\": \"integer\"\n          },\n          \"parameters\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n              \".*\": {\n                \"type\": \"number\",\n                \"maximum\": 1.0,\n                \"minimum\": 0.0\n              }\n            }\n          },\n          \"properties\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n              \".*\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        },\n        \"required\": [\"processor\"],\n        \"additionalProperties\": false\n      }\n    }\n  }\n}\n)\""
  },
  {
    "path": "src/engine/json_schemas/tracks_schema.json",
    "content": "R\"(\n{\n  \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n  \"title\": \"Sushi Plugin Chain JSON Schema\",\n  \"description\": \"JSON Schema to validate track definitions\",\n\n  \"definitions\" : {\n    \"internal_plugin\" :\n    {\n      \"type\" : \"object\",\n      \"properties\": {\n        \"uid\":{\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        \"name\":{\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        \"type\":{\n          \"enum\": [\"internal\"]\n        }\n      },\n      \"required\": [\"uid\", \"name\", \"type\"]\n    },\n    \"vst2x_plugin\" :\n    {\n      \"type\" : \"object\",\n      \"properties\": {\n        \"path\":{\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        \"name\":{\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        \"type\":{\n          \"enum\": [\"vst2x\"]\n        }\n      },\n      \"required\": [\"path\", \"name\", \"type\"]\n    },\n    \"vst3x_plugin\" :\n    {\n      \"type\" : \"object\",\n      \"properties\": {\n        \"uid\":{\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        \"path\":{\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        \"name\":{\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        \"type\":{\n          \"enum\": [\"vst3x\"]\n        }\n      },\n      \"required\": [\"uid\", \"path\", \"name\", \"type\"]\n    },\n    \"lv2_plugin\" :\n    {\n      \"type\" : \"object\",\n      \"properties\": {\n        \"uri\":{\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        \"name\":{\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        \"type\":{\n          \"enum\": [\"lv2\"]\n        }\n      },\n      \"required\": [\"uri\", \"name\", \"type\"]\n    },\n    \"bus_connection\":{\n      \"type\": \"object\",\n      \"properties\":\n      {\n        \"engine_bus\":\n        {\n          \"type\": \"integer\",\n          \"minimum\": 0\n        },\n        \"track_bus\":\n        {\n          \"type\": \"integer\",\n          \"minimum\": 0\n        }\n      },\n      \"required\": [\"engine_bus\",\"track_bus\"]\n    },\n    \"channel_connection\":{\n      \"type\": \"object\",\n      \"properties\":\n      {\n        \"engine_channel\":\n        {\n          \"type\": \"integer\",\n          \"minimum\": 0\n        },\n        \"track_channel\":\n        {\n          \"type\": \"integer\",\n          \"minimum\": 0\n        }\n      },\n      \"required\": [\"engine_channel\",\"track_channel\"]\n    },\n    \"track\" :{\n      \"type\": \"object\",\n      \"properties\":\n      {\n        \"name\":\n        {\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        \"multibus\" :\n        {\n          \"type\" : \"boolean\"\n        },\n        \"channels\" :\n        {\n          \"type\": \"integer\",\n          \"minimum\" : 0\n        },\n        \"buses\" :\n        {\n          \"type\": \"integer\",\n          \"minimum\":  0\n        },\n        \"inputs\":\n        {\n          \"type\": \"array\",\n          \"items\":\n          {\n            \"type\": \"object\",\n            \"oneOf\":\n            [\n              {\"$ref\": \"#/definitions/bus_connection\"},\n              {\"$ref\": \"#/definitions/channel_connection\"}\n            ]\n          }\n        },\n        \"outputs\":\n        {\n          \"type\": \"array\",\n          \"items\":\n          {\n            \"type\": \"object\",\n            \"oneOf\":\n            [\n              {\"$ref\": \"#/definitions/bus_connection\"},\n              {\"$ref\": \"#/definitions/channel_connection\"}\n            ]\n          }\n        },\n        \"thread\":\n        {\n          \"type\": \"integer\",\n          \"minimum\": 0\n        },\n        \"plugins\":\n        {\n          \"type\": \"array\",\n          \"items\":\n          {\n            \"type\": \"object\",\n            \"oneOf\":\n            [\n              {\"$ref\": \"#/definitions/internal_plugin\"},\n              {\"$ref\": \"#/definitions/vst2x_plugin\"},\n              {\"$ref\": \"#/definitions/vst3x_plugin\"},\n              {\"$ref\": \"#/definitions/lv2_plugin\"}\n            ]\n          }\n        }\n      },\n      \"anyOf\": [{\n        \"required\": [\"name\", \"channels\", \"inputs\", \"outputs\", \"plugins\"]\n      }, {\n        \"required\": [\"name\", \"multibus\", \"buses\", \"inputs\", \"outputs\", \"plugins\"]\n      }]\n    },\n    \"pre_post_track\" :{\n      \"type\": \"object\",\n      \"properties\":\n      {\n        \"name\":\n        {\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        \"plugins\":\n        {\n          \"type\": \"array\",\n          \"items\":\n          {\n            \"type\": \"object\",\n            \"oneOf\":\n            [\n              {\"$ref\": \"#/definitions/internal_plugin\"},\n              {\"$ref\": \"#/definitions/vst2x_plugin\"},\n              {\"$ref\": \"#/definitions/vst3x_plugin\"},\n              {\"$ref\": \"#/definitions/lv2_plugin\"}\n            ]\n          }\n        }\n      },\n      \"required\": [\"name\", \"plugins\"]\n    }\n  },\n  \"type\": \"object\",\n  \"properties\":\n  {\n    \"tracks\":\n    {\n      \"type\": \"array\",\n      \"items\":\n      {\n        \"$ref\": \"#/definitions/track\"\n      },\n      \"minItems\": 1\n    },\n    \"pre_track\":\n    {\n      \"$ref\": \"#/definitions/pre_post_track\"\n    },\n    \"post_track\":\n    {\n      \"$ref\": \"#/definitions/pre_post_track\"\n    }\n  },\n  \"required\": [\"tracks\"]\n}\n)\""
  },
  {
    "path": "src/engine/link_dummy.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief No-op version of the Ableton Link interface for use when sushi is\n * not compiled with Link support.\n */\n#ifndef SUSHI_LINK_DUMMY_H\n#define SUSHI_LINK_DUMMY_H\n\n#include <chrono>\n\nnamespace sushi::internal::engine {\n\nclass SushiLink\n{\npublic:\n  using Clock = int;\n  class SessionState;\n\n  explicit SushiLink(double /*bpm*/) {}\n  bool isEnabled() const {return false;}\n  void enable(bool /*bEnable*/) {}\n  bool isStartStopSyncEnabled() const {return false;}\n  void enableStartStopSync(bool /*bEnable*/) {}\n  std::size_t numPeers() const {return 0;}\n\n  template <typename Callback>\n  void setNumPeersCallback(Callback /*callback*/) {}\n\n  template <typename Callback>\n  void setTempoCallback(Callback /*callback*/) {}\n\n  template <typename Callback>\n  void setStartStopCallback(Callback /*callback*/) {}\n\n  Clock clock() const {return 0;}\n  SessionState captureAudioSessionState() const {return {};}\n  void commitAudioSessionState(SessionState /*state*/) {}\n  SessionState captureAppSessionState() const {return {};}\n  void commitAppSessionState(SessionState /*state*/) {}\n  class SessionState\n  {\n  public:\n    SessionState() = default;\n    double tempo() const {return 120;}\n    void setTempo(double /*bpm*/, std::chrono::microseconds /*atTime*/) {}\n    double beatAtTime(std::chrono::microseconds /*time*/, double /*quantum*/) const {return 0;}\n    double phaseAtTime(std::chrono::microseconds /*time*/, double /*quantum*/) const {return 0;}\n    std::chrono::microseconds timeAtBeat(double /*beat*/, double /*quantum*/) const {return std::chrono::seconds(0);}\n    void requestBeatAtTime(double /*beat*/, std::chrono::microseconds /*time*/, double /*quantum*/) {}\n    void forceBeatAtTime(double /*beat*/, std::chrono::microseconds /*time*/, double /*quantum*/) {}\n    void setIsPlaying(bool /*isPlaying*/, std::chrono::microseconds /*time*/) {}\n    bool isPlaying() const {return false;}\n    std::chrono::microseconds timeForIsPlaying() const {return std::chrono::seconds(0);}\n    void requestBeatAtStartPlayingTime(double /*beat*/, double /*quantum*/) {}\n    void setIsPlayingAndRequestBeatAtTime(bool /*isPlaying*/, std::chrono::microseconds /*time*/,\n                                          double /*beat*/, double /*quantum*/) {}\n\n  private:\n    friend SushiLink;\n  };\nprivate:\n};\n} // end namespace sushi::engine\n\n\n#endif // SUSHI_LINK_DUMMY_H\n"
  },
  {
    "path": "src/engine/midi_dispatcher.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Handles translation of midi to internal events and midi routing\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <algorithm>\n\n#include \"elklog/static_logger.h\"\n\n#include \"engine/midi_dispatcher.h\"\n\n#include \"base_event_dispatcher.h\"\n#include \"base_engine.h\"\n#include \"library/midi_encoder.h\"\n\nnamespace sushi::internal::midi_dispatcher {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"midi dispatcher\");\n\ninline std::unique_ptr<Event> make_note_on_event(const InputConnection& c,\n                                                 const midi::NoteOnMessage& msg,\n                                                 Time timestamp)\n{\n    if (msg.velocity == 0)\n    {\n        return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::NOTE_OFF, c.target, msg.channel, msg.note, 0.5f, timestamp);\n    }\n\n    float velocity = msg.velocity / static_cast<float>(midi::MAX_VALUE);\n    return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::NOTE_ON, c.target, msg.channel, msg.note, velocity, timestamp);\n}\n\ninline std::unique_ptr<Event> make_note_off_event(const InputConnection& c,\n                                                  const midi::NoteOffMessage& msg,\n                                                  Time timestamp)\n{\n    float velocity = msg.velocity / static_cast<float>(midi::MAX_VALUE);\n    return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::NOTE_OFF, c.target, msg.channel, msg.note, velocity, timestamp);\n}\n\ninline std::unique_ptr<Event> make_note_aftertouch_event(const InputConnection& c,\n                                                         const midi::PolyKeyPressureMessage& msg,\n                                                         Time timestamp)\n{\n    float pressure = msg.pressure / static_cast<float>(midi::MAX_VALUE);\n    return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::NOTE_AFTERTOUCH, c.target, msg.channel, msg.note, pressure, timestamp);\n}\n\ninline std::unique_ptr<Event> make_aftertouch_event(const InputConnection& c,\n                                                    const midi::ChannelPressureMessage& msg,\n                                                    Time timestamp)\n{\n    float pressure = msg.pressure / static_cast<float>(midi::MAX_VALUE);\n    return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::AFTERTOUCH, c.target, msg.channel, pressure, timestamp);\n}\n\ninline std::unique_ptr<Event> make_modulation_event(const InputConnection& c,\n                                                    const midi::ControlChangeMessage& msg,\n                                                    Time timestamp)\n{\n    float value = msg.value / static_cast<float>(midi::MAX_VALUE);\n    return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::MODULATION, c.target, msg.channel, value, timestamp);\n}\n\ninline std::unique_ptr<Event> make_pitch_bend_event(const InputConnection& c,\n                                                    const midi::PitchBendMessage& msg,\n                                                    Time timestamp)\n{\n    float value = (msg.value / static_cast<float>(midi::PITCH_BEND_MIDDLE)) - 1.0f;\n    return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::PITCH_BEND, c.target, msg.channel, value, timestamp);\n}\n\ninline std::unique_ptr<Event> make_wrapped_midi_event(const InputConnection& c,\n                                                      const uint8_t* data,\n                                                      size_t size,\n                                                      Time timestamp)\n{\n    MidiDataByte midi_data{0};\n    std::copy(data, data + size, midi_data.data());\n    return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::WRAPPED_MIDI, c.target, midi_data, timestamp);\n}\n\ninline std::unique_ptr<Event> make_param_change_event(InputConnection& c,\n                                                      const midi::ControlChangeMessage& msg,\n                                                      Time timestamp)\n{\n    uint8_t abs_value = msg.value;\n    // TODO: Maybe? Currently this is based on a virtual controller absolute value which is\n    //  initialized at 64. An alternative would be to read the parameter value from the plugin\n    //  and compute a change from that. We should investigate what other DAWs are doing.\n    if (c.relative)\n    {\n        abs_value = c.virtual_abs_value;\n        if (msg.value < 64u)\n        {\n            auto clipped_increment = std::min<uint8_t>(msg.value, 127u - abs_value);\n            abs_value += clipped_increment;\n        }\n        else\n        {\n            // Two-complement encoding for negative relative changes\n            auto clipped_decrease = std::min<uint8_t>(128u - msg.value, abs_value);\n            abs_value -= clipped_decrease;\n        }\n        c.virtual_abs_value = abs_value;\n    }\n    float value = static_cast<float>(abs_value) / midi::MAX_VALUE * (c.max_range - c.min_range) + c.min_range;\n    return std::make_unique<ParameterChangeEvent>(ParameterChangeEvent::Subtype::FLOAT_PARAMETER_CHANGE, c.target, c.parameter, value, timestamp);\n}\n\ninline std::unique_ptr<Event> make_program_change_event(const InputConnection& c,\n                                                        const midi::ProgramChangeMessage& msg,\n                                                        Time timestamp)\n{\n    return std::make_unique<ProgramChangeEvent>(c.target, msg.program, timestamp);\n}\n\nMidiDispatcher::MidiDispatcher(dispatcher::BaseEventDispatcher* event_dispatcher) : _frontend(nullptr),\n                                                                                    _event_dispatcher(event_dispatcher)\n{\n    _event_dispatcher->subscribe_to_keyboard_events(this);\n    _event_dispatcher->subscribe_to_engine_notifications(this);\n}\n\nMidiDispatcher::~MidiDispatcher()\n{\n    _event_dispatcher->unsubscribe_from_keyboard_events(this);\n    _event_dispatcher->unsubscribe_from_engine_notifications(this);\n}\n\n\nvoid MidiDispatcher::set_midi_outputs(int no_outputs)\n{\n    _midi_outputs = no_outputs;\n    _enabled_clock_out = std::vector<int>(no_outputs, 0);\n}\n\nMidiDispatcherStatus MidiDispatcher::connect_cc_to_parameter(int midi_input,\n                                                             ObjectId processor_id,\n                                                             ObjectId parameter_id,\n                                                             int cc_no,\n                                                             float min_range,\n                                                             float max_range,\n                                                             bool use_relative_mode,\n                                                             int channel)\n{\n    if (midi_input >= _midi_inputs || midi_input < 0 || channel > midi::MidiChannel::OMNI)\n    {\n        return MidiDispatcherStatus ::INVALID_MIDI_INPUT;\n    }\n\n    InputConnection connection;\n    connection.target = processor_id;\n    connection.parameter = parameter_id;\n    connection.min_range = min_range;\n    connection.max_range = max_range;\n    connection.relative = use_relative_mode;\n    connection.virtual_abs_value = 64;\n\n    std::scoped_lock lock(_cc_routes_lock);\n\n    _cc_routes[midi_input][cc_no][channel].push_back(connection);\n    ELKLOG_LOG_INFO(\"Connected parameter ID \\\"{}\\\" \"\n                           \"(cc number \\\"{}\\\") to processor ID \\\"{}\\\"\", parameter_id, cc_no, processor_id);\n    return MidiDispatcherStatus::OK;\n}\n\nMidiDispatcherStatus MidiDispatcher::disconnect_cc_from_parameter(int midi_input,\n                                                                  ObjectId processor_id,\n                                                                  int cc_no,\n                                                                  int channel)\n{\n    if (midi_input >= _midi_inputs || midi_input < 0 || channel > midi::MidiChannel::OMNI)\n    {\n        return MidiDispatcherStatus::INVALID_MIDI_INPUT;\n    }\n\n    std::scoped_lock lock(_cc_routes_lock);\n\n    auto connections = _cc_routes.find(midi_input);\n    if (connections != _cc_routes.end())\n    {\n        auto& connection_vector = connections->second[cc_no][channel];\n        auto erase_iterator = std::remove_if(connection_vector.begin(),\n                                             connection_vector.end(),\n                                             [&](const auto& c)\n                                             {\n                                                 return c.target == processor_id;\n                                             });\n\n        connection_vector.erase(erase_iterator, connection_vector.end());\n    }\n\n    ELKLOG_LOG_INFO(\"Disconnected \"\n                   \"(cc number \\\"{}\\\") from processor ID \\\"{}\\\"\", cc_no, processor_id);\n    return MidiDispatcherStatus::OK;\n}\n\nMidiDispatcherStatus MidiDispatcher::disconnect_all_cc_from_processor(ObjectId processor_id)\n{\n    std::scoped_lock lock(_cc_routes_lock);\n\n    for(auto input_i = _cc_routes.begin(); input_i != _cc_routes.end(); ++input_i)\n    {\n        auto& cc_channel_matrix = input_i->second;\n        for(size_t cc_i = 0 ; cc_i < cc_channel_matrix.size(); ++cc_i)\n        {\n            auto& channels = cc_channel_matrix[cc_i];\n            for(size_t channel_i = 0 ; channel_i < channels.size(); ++channel_i)\n            {\n                auto& connection_vector = cc_channel_matrix[cc_i][channel_i];\n                auto erase_iterator = std::remove_if(connection_vector.begin(),\n                                                     connection_vector.end(),\n                                                     [&](const auto& c)\n                                                     {\n                                                         return c.target == processor_id;\n                                                     });\n\n                if (erase_iterator != connection_vector.end())\n                {\n                    connection_vector.erase(erase_iterator, connection_vector.end());\n                    ELKLOG_LOG_DEBUG(\"Disconnected all CC's from processor ID {}\", processor_id);\n                }\n            }\n        }\n    }\n\n    return MidiDispatcherStatus::OK;\n}\n\nstd::vector<CCInputConnection> MidiDispatcher::get_all_cc_input_connections()\n{\n    return _get_cc_input_connections(std::nullopt);\n}\n\nstd::vector<CCInputConnection> MidiDispatcher::get_cc_input_connections_for_processor(int processor_id)\n{\n    return _get_cc_input_connections(processor_id);\n}\n\nMidiDispatcherStatus MidiDispatcher::connect_pc_to_processor(int midi_input,\n                                                             ObjectId processor_id,\n                                                             int channel)\n{\n    if (midi_input >= _midi_inputs || midi_input < 0 || channel > midi::MidiChannel::OMNI)\n    {\n        return MidiDispatcherStatus::INVALID_MIDI_INPUT;\n    }\n\n    InputConnection connection;\n    connection.target = processor_id;\n    connection.parameter = 0;\n    connection.min_range = 0;\n    connection.max_range = 0;\n\n    std::scoped_lock lock(_pc_routes_lock);\n\n    _pc_routes[midi_input][channel].push_back(connection);\n    ELKLOG_LOG_INFO(\"Connected program changes from MIDI port \\\"{}\\\" to processor id\\\"{}\\\"\", midi_input, processor_id);\n    return MidiDispatcherStatus::OK;\n}\n\nMidiDispatcherStatus MidiDispatcher::disconnect_pc_from_processor(int midi_input,\n                                                                  ObjectId processor_id,\n                                                                  int channel)\n{\n    if (midi_input >= _midi_inputs || midi_input < 0 || channel > midi::MidiChannel::OMNI)\n    {\n        return MidiDispatcherStatus::INVALID_MIDI_INPUT;\n    }\n\n    std::scoped_lock lock(_pc_routes_lock);\n\n    auto connections = _pc_routes.find(midi_input);\n    if (connections != _pc_routes.end())\n    {\n        auto& connection_vector = connections->second[channel];\n        auto erase_iterator = std::remove_if(connection_vector.begin(),\n                                             connection_vector.end(),\n                                             [&](const auto& c)\n                                             {\n                                                 return c.target == processor_id;\n                                             });\n\n        connection_vector.erase(erase_iterator, connection_vector.end());\n    }\n\n    ELKLOG_LOG_INFO(\"Disconnected program changes from MIDI port \\\"{}\\\" to processor ID \\\"{}\\\"\", midi_input, processor_id);\n    return MidiDispatcherStatus::OK;\n}\n\nMidiDispatcherStatus MidiDispatcher::disconnect_all_pc_from_processor(ObjectId processor_id)\n{\n    std::scoped_lock lock(_pc_routes_lock);\n\n    for(auto inputs_i = _pc_routes.begin(); inputs_i != _pc_routes.end(); ++inputs_i)\n    {\n        auto& channels = inputs_i->second;\n        for(size_t channel_i = 0 ; channel_i < channels.size(); ++channel_i)\n        {\n            auto& connection_vector = channels[channel_i];\n            auto erase_iterator = std::remove_if(connection_vector.begin(),\n                                                 connection_vector.end(),\n                                                 [&](const auto& c)\n                                                 {\n                                                     return c.target == processor_id;\n                                                 });\n\n            connection_vector.erase(erase_iterator, connection_vector.end());\n        }\n    }\n    ELKLOG_LOG_DEBUG(\"Disconnected all PC's from processor ID \\\"{}\\\"\", processor_id);\n\n    return MidiDispatcherStatus::OK;\n}\n\nstd::vector<PCInputConnection> MidiDispatcher::get_all_pc_input_connections()\n{\n    return _get_pc_input_connections(std::nullopt);\n}\n\nstd::vector<PCInputConnection> MidiDispatcher::get_pc_input_connections_for_processor(int processor_id)\n{\n    return _get_pc_input_connections(processor_id);\n}\n\nMidiDispatcherStatus MidiDispatcher::connect_kb_to_track(int midi_input,\n                                                         ObjectId track_id,\n                                                         int channel)\n{\n    if (midi_input >= _midi_inputs || midi_input < 0 || channel > midi::MidiChannel::OMNI)\n    {\n        return MidiDispatcherStatus::INVALID_MIDI_INPUT;\n    }\n\n    InputConnection connection;\n    connection.target = track_id;\n    connection.parameter = 0;\n    connection.min_range = 0;\n    connection.max_range = 0;\n\n    std::scoped_lock lock(_kb_routes_in_lock);\n\n    _kb_routes_in[midi_input][channel].push_back(connection);\n    ELKLOG_LOG_INFO(\"Connected MIDI port \\\"{}\\\" to track ID \\\"{}\\\"\", midi_input, track_id);\n    return MidiDispatcherStatus::OK;\n}\n\nMidiDispatcherStatus MidiDispatcher::disconnect_kb_from_track(int midi_input,\n                                                              ObjectId track_id,\n                                                              int channel)\n{\n    // TODO: These midi_input checks here and elsewhere should be made redundant eventually - when _midi_inputs isn't used.\n    if (midi_input >= _midi_inputs || midi_input < 0 || channel > midi::MidiChannel::OMNI)\n    {\n        return MidiDispatcherStatus::INVALID_MIDI_INPUT;\n    }\n\n    std::scoped_lock lock(_kb_routes_in_lock);\n\n    auto connections = _kb_routes_in.find(midi_input); // All connections for the midi_input\n    if (connections != _kb_routes_in.end())\n    {\n        auto& connection_vector = connections->second[channel];\n        auto erase_iterator = std::remove_if(connection_vector.begin(),\n                                             connection_vector.end(),\n                                             [&](const auto& c)\n                                             {\n                                                 return c.target == track_id;\n                                             });\n\n        connection_vector.erase(erase_iterator, connection_vector.end());\n    }\n\n    ELKLOG_LOG_INFO(\"Disconnected MIDI port \\\"{}\\\" from track ID \\\"{}\\\"\", midi_input, track_id);\n    return MidiDispatcherStatus::OK;\n}\n\nstd::vector<KbdInputConnection> MidiDispatcher::get_all_kb_input_connections()\n{\n    std::vector<KbdInputConnection> returns;\n\n    std::scoped_lock lock_in(_kb_routes_in_lock);\n\n    // Adding kbd connections:\n    for(auto inputs_i = _kb_routes_in.begin(); inputs_i != _kb_routes_in.end(); ++inputs_i)\n    {\n        auto& channels = inputs_i->second;\n        for(size_t channel_i = 0 ; channel_i < channels.size(); ++channel_i)\n        {\n            for(auto connection = channels[channel_i].begin(); connection != channels[channel_i].end(); ++connection)\n            {\n                KbdInputConnection conn {};\n                conn.input_connection = *connection;\n                conn.port = inputs_i->first;\n                conn.channel = static_cast<int>(channel_i);\n                conn.raw_midi = false;\n                returns.emplace_back(conn);\n            }\n        }\n    }\n\n    std::scoped_lock lock_raw(_raw_routes_in_lock);\n\n    // Adding Raw midi connections:\n    for(auto inputs_i = _raw_routes_in.begin(); inputs_i != _raw_routes_in.end(); ++inputs_i)\n    {\n        auto channels = inputs_i->second;\n        for(size_t channel_i = 0 ; channel_i < channels.size(); ++channel_i)\n        {\n            for(auto connection = channels[channel_i].begin(); connection != channels[channel_i].end(); ++connection)\n            {\n                KbdInputConnection conn {};\n                conn.input_connection = *connection;\n                conn.port = inputs_i->first;\n                conn.channel = static_cast<int>(channel_i);\n                conn.raw_midi = true;\n                returns.emplace_back(conn);\n            }\n        }\n    }\n\n    return returns;\n}\n\nMidiDispatcherStatus MidiDispatcher::connect_raw_midi_to_track(int midi_input,\n                                                               ObjectId track_id,\n                                                               int channel)\n{\n    if (midi_input >= _midi_inputs || midi_input < 0 || channel > midi::MidiChannel::OMNI)\n    {\n        return MidiDispatcherStatus::INVALID_MIDI_INPUT;\n    }\n\n    InputConnection connection;\n    connection.target = track_id;\n    connection.parameter = 0;\n    connection.min_range = 0;\n    connection.max_range = 0;\n\n    std::scoped_lock lock(_raw_routes_in_lock);\n\n    _raw_routes_in[midi_input][channel].push_back(connection);\n    ELKLOG_LOG_INFO(\"Connected MIDI port \\\"{}\\\" to track ID \\\"{}\\\"\", midi_input, track_id);\n    return MidiDispatcherStatus::OK;\n}\n\nMidiDispatcherStatus MidiDispatcher::disconnect_raw_midi_from_track(int midi_input,\n                                                                    ObjectId track_id,\n                                                                    int channel)\n{\n    if (midi_input >= _midi_inputs || midi_input < 0 || channel > midi::MidiChannel::OMNI)\n    {\n        return MidiDispatcherStatus::INVALID_MIDI_INPUT;\n    }\n\n    std::scoped_lock lock(_raw_routes_in_lock);\n\n    auto connections = _raw_routes_in.find(midi_input); // All connections for the midi_input\n    if (connections != _raw_routes_in.end())\n    {\n        auto& connection_vector = connections->second[channel];\n        auto erase_iterator = std::remove_if(connection_vector.begin(),\n                                             connection_vector.end(),\n                                             [&](const auto& c)\n                                             {\n                                                 return c.target == track_id;\n                                             });\n\n        connection_vector.erase(erase_iterator, connection_vector.end());\n    }\n\n    ELKLOG_LOG_INFO(\"Disconnected MIDI port \\\"{}\\\" from track ID \\\"{}\\\"\", midi_input, track_id);\n    return MidiDispatcherStatus::OK;\n}\n\nMidiDispatcherStatus MidiDispatcher::connect_track_to_output(int midi_output, ObjectId track_id, int channel)\n{\n    if (channel >= midi::MidiChannel::OMNI)\n    {\n        return MidiDispatcherStatus::INVAlID_CHANNEL;\n    }\n    if (midi_output >= _midi_outputs || midi_output < 0)\n    {\n        return MidiDispatcherStatus::INVALID_MIDI_OUTPUT;\n    }\n\n    OutputConnection connection;\n    connection.channel = channel;\n    connection.output = midi_output;\n\n    // TODO: Why are these here? Why not remove from OutputConnection struct?\n    connection.min_range = 1.234f;\n    connection.max_range = 4.5678f;\n\n    connection.cc_number = 123;\n\n    std::scoped_lock lock(_kb_routes_out_lock);\n\n    _kb_routes_out[track_id].push_back(connection);\n    ELKLOG_LOG_INFO(\"Connected MIDI from track ID \\\"{}\\\" to port \\\"{}\\\" with channel {}\", track_id, midi_output, channel);\n    return MidiDispatcherStatus::OK;\n}\n\nMidiDispatcherStatus MidiDispatcher::disconnect_track_from_output(int midi_output,\n                                                                  ObjectId track_id,\n                                                                  int channel)\n{\n    if (channel >= midi::MidiChannel::OMNI)\n    {\n        return MidiDispatcherStatus::INVAlID_CHANNEL;\n    }\n    if (midi_output >= _midi_outputs || midi_output < 0)\n    {\n        return MidiDispatcherStatus::INVALID_MIDI_OUTPUT;\n    }\n\n    std::scoped_lock lock(_kb_routes_out_lock);\n\n    auto connections = _kb_routes_out.find(track_id);\n    if (connections != _kb_routes_out.end())\n    {\n        auto& connection_vector = connections->second;\n        auto erase_iterator = std::remove_if(connection_vector.begin(),\n                                             connection_vector.end(),\n                                             [&](const auto& c)\n                                             {\n                                                 return (c.channel == channel) && (c.output == midi_output);\n                                             });\n\n        connection_vector.erase(erase_iterator, connection_vector.end());\n    }\n\n    ELKLOG_LOG_INFO(\"Disconnected MIDI from track ID \\\"{}\\\" to port \\\"{}\\\" with channel {}\", track_id, midi_output, channel);\n    return MidiDispatcherStatus::OK;\n}\n\nstd::vector<KbdOutputConnection> MidiDispatcher::get_all_kb_output_connections()\n{\n    std::vector<KbdOutputConnection> returns;\n\n    std::scoped_lock lock(_kb_routes_out_lock);\n\n    for(auto outputs_i = _kb_routes_out.begin(); outputs_i != _kb_routes_out.end(); ++outputs_i)\n    {\n        for (auto connection = outputs_i->second.begin(); connection != outputs_i->second.end(); ++connection)\n        {\n            KbdOutputConnection conn;\n            conn.track_id = outputs_i->first;\n            conn.port = connection->output;\n            conn.channel = connection->channel;\n            returns.emplace_back(conn);\n        }\n    }\n\n    return returns;\n}\n\nMidiDispatcherStatus MidiDispatcher::enable_midi_clock(bool enabled, int midi_output)\n{\n    if (static_cast<size_t>(midi_output) < _enabled_clock_out.size())\n    {\n        _enabled_clock_out[midi_output] = enabled;\n        return MidiDispatcherStatus::OK;\n    }\n    ELKLOG_LOG_ERROR(\"Failed to {} midi clock for port {}, no such port\", enabled? \"enable\" : \"disable\", midi_output);\n    return MidiDispatcherStatus::INVALID_MIDI_OUTPUT;\n}\n\nbool MidiDispatcher::midi_clock_enabled(int midi_output)\n{\n    if (static_cast<size_t>(midi_output) < _enabled_clock_out.size())\n    {\n        return _enabled_clock_out[midi_output];\n    }\n    return false;\n}\n\nvoid MidiDispatcher::send_midi(int port, MidiDataByte data, Time timestamp)\n{\n    const int channel = midi::decode_channel(data);\n    const int size = static_cast<int>(data.size());\n    /* Dispatch raw midi messages */\n    {\n        std::scoped_lock lock(_raw_routes_in_lock);\n\n        const auto& raw_cons_in = _raw_routes_in.find(port);\n        if (raw_cons_in != _raw_routes_in.end())\n        {\n            for (auto c : raw_cons_in->second[midi::MidiChannel::OMNI])\n            {\n                _event_dispatcher->post_event(make_wrapped_midi_event(c, data.data(), size, timestamp));\n            }\n            for (auto c : raw_cons_in->second[channel])\n            {\n                _event_dispatcher->post_event(make_wrapped_midi_event(c, data.data(), size, timestamp));\n            }\n        }\n    }\n\n    std::scoped_lock kb_lock(_kb_routes_in_lock);\n\n    /* Dispatch decoded midi messages */\n    midi::MessageType type = midi::decode_message_type(data);\n    switch (type)\n    {\n        case midi::MessageType::CONTROL_CHANGE:\n        {\n            midi::ControlChangeMessage decoded_msg = midi::decode_control_change(data);\n\n            std::scoped_lock cc_lock(_cc_routes_lock);\n\n            const auto& cc_cons = _cc_routes.find(port);\n            if (cc_cons != _cc_routes.end())\n            {\n                for (auto& c : cc_cons->second[decoded_msg.controller][midi::MidiChannel::OMNI])\n                {\n                    _event_dispatcher->post_event(make_param_change_event(c, decoded_msg, timestamp));\n                }\n                for (auto& c : cc_cons->second[decoded_msg.controller][decoded_msg.channel])\n                {\n                    _event_dispatcher->post_event(make_param_change_event(c, decoded_msg, timestamp));\n                }\n            }\n            if (decoded_msg.controller == midi::MOD_WHEEL_CONTROLLER_NO)\n            {\n                const auto& cons = _kb_routes_in.find(port);\n                if (cons != _kb_routes_in.end())\n                {\n                    for (auto c : cons->second[midi::MidiChannel::OMNI])\n                    {\n                        _event_dispatcher->post_event(make_modulation_event(c, decoded_msg, timestamp));\n                    }\n                    for (auto c : cons->second[decoded_msg.channel])\n                    {\n                        _event_dispatcher->post_event(make_modulation_event(c, decoded_msg, timestamp));\n                    }\n                }\n            }\n            break;\n        }\n\n        case midi::MessageType::NOTE_ON:\n        {\n            midi::NoteOnMessage decoded_msg = midi::decode_note_on(data);\n            const auto& cons = _kb_routes_in.find(port);\n            if (cons != _kb_routes_in.end())\n            {\n                for (auto c : cons->second[midi::MidiChannel::OMNI])\n                {\n                    _event_dispatcher->post_event(make_note_on_event(c, decoded_msg, timestamp));\n                }\n                for (auto c : cons->second[decoded_msg.channel])\n                {\n                    _event_dispatcher->post_event(make_note_on_event(c, decoded_msg, timestamp));\n                }\n            }\n            break;\n        }\n\n        case midi::MessageType::NOTE_OFF:\n        {\n            midi::NoteOffMessage decoded_msg = midi::decode_note_off(data);\n            const auto& cons = _kb_routes_in.find(port);\n            if (cons != _kb_routes_in.end())\n            {\n                for (auto c : cons->second[midi::MidiChannel::OMNI])\n                {\n                    _event_dispatcher->post_event(make_note_off_event(c, decoded_msg, timestamp));\n                }\n                for (auto c : cons->second[decoded_msg.channel])\n                {\n                    _event_dispatcher->post_event(make_note_off_event(c, decoded_msg, timestamp));\n                }\n            }\n            break;\n        }\n\n        case midi::MessageType::PITCH_BEND:\n        {\n            midi::PitchBendMessage decoded_msg = midi::decode_pitch_bend(data);\n            const auto& cons = _kb_routes_in.find(port);\n            if (cons != _kb_routes_in.end())\n            {\n                for (auto c : cons->second[midi::MidiChannel::OMNI])\n                {\n                    _event_dispatcher->post_event(make_pitch_bend_event(c, decoded_msg, timestamp));\n                }\n                for (auto c : cons->second[decoded_msg.channel])\n                {\n                    _event_dispatcher->post_event(make_pitch_bend_event(c, decoded_msg, timestamp));\n                }\n            }\n            break;\n        }\n\n        case midi::MessageType::POLY_KEY_PRESSURE:\n        {\n            midi::PolyKeyPressureMessage decoded_msg = midi::decode_poly_key_pressure(data);\n            const auto& cons = _kb_routes_in.find(port);\n            if (cons != _kb_routes_in.end())\n            {\n                for (auto c : cons->second[midi::MidiChannel::OMNI])\n                {\n                    _event_dispatcher->post_event(make_note_aftertouch_event(c, decoded_msg, timestamp));\n                }\n                for (auto c : cons->second[decoded_msg.channel])\n                {\n                    _event_dispatcher->post_event(make_note_aftertouch_event(c, decoded_msg, timestamp));\n                }\n            }\n            break;\n        }\n\n        case midi::MessageType::CHANNEL_PRESSURE:\n        {\n            midi::ChannelPressureMessage decoded_msg = midi::decode_channel_pressure(data);\n            const auto& cons = _kb_routes_in.find(port);\n            if (cons != _kb_routes_in.end())\n            {\n                for (auto c : cons->second[midi::MidiChannel::OMNI])\n                {\n                    _event_dispatcher->post_event(make_aftertouch_event(c, decoded_msg, timestamp));\n                }\n                for (auto c : cons->second[decoded_msg.channel])\n                {\n                    _event_dispatcher->post_event( make_aftertouch_event(c, decoded_msg, timestamp));\n                }\n            }\n            break;\n        }\n\n        case midi::MessageType::PROGRAM_CHANGE:\n        {\n            midi::ProgramChangeMessage decoded_msg = midi::decode_program_change(data);\n\n            std::scoped_lock lock(_pc_routes_lock);\n            const auto& cons = _pc_routes.find(port);\n            if (cons != _pc_routes.end())\n            {\n                for (auto c : cons->second[midi::MidiChannel::OMNI])\n                {\n                    _event_dispatcher->post_event(make_program_change_event(c, decoded_msg, timestamp));\n                }\n                for (auto c : cons->second[decoded_msg.channel])\n                {\n                    _event_dispatcher->post_event(make_program_change_event(c, decoded_msg, timestamp));\n                }\n            }\n            break;\n        }\n\n        default:\n            break;\n    }\n}\n\nint MidiDispatcher::process(Event* event)\n{\n    if (event->is_keyboard_event())\n    {\n        std::scoped_lock lock(_kb_routes_out_lock);\n        auto typed_event = static_cast<KeyboardEvent*>(event);\n        const auto& cons = _kb_routes_out.find(typed_event->processor_id());\n        if (cons != _kb_routes_out.end())\n        {\n            for (const OutputConnection& c : cons->second)\n            {\n                MidiDataByte midi_data;\n                switch (typed_event->subtype())\n                {\n                    case KeyboardEvent::Subtype::NOTE_ON:\n                        midi_data = midi::encode_note_on(c.channel, typed_event->note(), typed_event->velocity());\n                        break;\n                    case KeyboardEvent::Subtype::NOTE_OFF:\n                        midi_data = midi::encode_note_off(c.channel, typed_event->note(), typed_event->velocity());\n                        break;\n                    case KeyboardEvent::Subtype::NOTE_AFTERTOUCH:\n                        midi_data = midi::encode_poly_key_pressure(c.channel, typed_event->note(), typed_event->velocity());\n                        break;\n                    case KeyboardEvent::Subtype::AFTERTOUCH:\n                        midi_data = midi::encode_channel_pressure(c.channel, typed_event->value());\n                        break;\n                    case KeyboardEvent::Subtype::PITCH_BEND:\n                        midi_data = midi::encode_pitch_bend(c.channel, typed_event->value());\n                        break;\n                    case KeyboardEvent::Subtype::MODULATION:\n                        midi_data = midi::encode_control_change(c.channel, midi::MOD_WHEEL_CONTROLLER_NO, typed_event->value());\n                        break;\n                    case KeyboardEvent::Subtype::WRAPPED_MIDI:\n                        midi_data = typed_event->midi_data();\n                }\n                ELKLOG_LOG_DEBUG(\"Dispatching midi [{:x} {:x} {:x} {:x}], timestamp: {}\",\n                                midi_data[0], midi_data[1], midi_data[2], midi_data[3], event->time().count());\n                _frontend->send_midi(c.output, midi_data, event->time());\n            }\n        }\n        return EventStatus::HANDLED_OK;\n    }\n    else if (event->is_engine_notification())\n    {\n        _handle_engine_notification(static_cast<EngineNotificationEvent*>(event));\n    }\n\n    return EventStatus::NOT_HANDLED;\n}\n\nstd::vector<CCInputConnection> MidiDispatcher::_get_cc_input_connections(std::optional<int> processor_id_filter)\n{\n    std::vector<CCInputConnection> returns;\n\n    std::scoped_lock lock(_cc_routes_lock);\n\n    for(auto input_i = _cc_routes.begin(); input_i != _cc_routes.end(); ++input_i)\n    {\n        auto& cc_channel_matrix = input_i->second;\n        for(size_t cc_i = 0 ; cc_i < cc_channel_matrix.size(); ++cc_i)\n        {\n            auto& channels = cc_channel_matrix[cc_i];\n            for(size_t channel_i = 0 ; channel_i < channels.size(); ++channel_i)\n            {\n                auto& connections = cc_channel_matrix[cc_i][channel_i];\n                for (auto connection = connections.begin(); connection != connections.end(); ++connection)\n                {\n                    if (!processor_id_filter.has_value() ||\n                        processor_id_filter == connection->target)\n                    {\n                        CCInputConnection conn {};\n                        conn.input_connection = *connection;\n                        conn.channel = static_cast<int>(channel_i);\n                        conn.port = input_i->first;\n                        conn.cc = static_cast<int>(cc_i);\n                        returns.emplace_back(conn);\n                    }\n                }\n            }\n        }\n    }\n\n    return returns;\n}\n\nstd::vector<PCInputConnection> MidiDispatcher::_get_pc_input_connections(std::optional<int> processor_id_filter)\n{\n    std::vector<PCInputConnection> returns;\n\n    std::scoped_lock lock(_pc_routes_lock);\n\n    for(auto inputs_i = _pc_routes.begin(); inputs_i != _pc_routes.end(); ++inputs_i)\n    {\n        auto& channels = inputs_i->second;\n        for(size_t channel_i = 0 ; channel_i < channels.size(); ++channel_i)\n        {\n            for(auto connection = channels[channel_i].begin(); connection != channels[channel_i].end(); ++connection)\n            {\n                if (!processor_id_filter.has_value() ||\n                    processor_id_filter == connection->target)\n                {\n                    PCInputConnection conn {};\n                    conn.processor_id = connection->target;\n                    conn.channel = static_cast<int>(channel_i);\n                    conn.port = inputs_i->first;\n                    returns.emplace_back(conn);\n                }\n            }\n        }\n    }\n\n    return returns;\n}\n\nbool MidiDispatcher::_handle_audio_graph_notification(const AudioGraphNotificationEvent* event)\n{\n    switch (event->action())\n    {\n        case AudioGraphNotificationEvent::Action::PROCESSOR_DELETED:\n        {\n            auto processor_id = event->processor();\n\n            disconnect_all_cc_from_processor(processor_id);\n\n            disconnect_all_pc_from_processor(processor_id);\n\n            ELKLOG_LOG_DEBUG(\"MidiController received a PROCESSOR_DELETED notification for processor {}\",\n                            event->processor());\n            break;\n        }\n        case AudioGraphNotificationEvent::Action::TRACK_DELETED:\n        {\n            auto track_id = event->track();\n\n            disconnect_all_cc_from_processor(track_id);\n            disconnect_all_pc_from_processor(track_id);\n\n            auto input_connections = get_all_kb_input_connections();\n            auto inputs_found = std::find_if(input_connections.begin(),\n                                             input_connections.end(),\n                                             [&](const auto& connection)\n                                             {\n                                                 return connection.input_connection.target == track_id;\n                                             });\n\n            while (inputs_found != input_connections.end())\n            {\n                disconnect_kb_from_track(inputs_found->port,\n                                         track_id,\n                                         inputs_found->channel);\n\n                disconnect_raw_midi_from_track(inputs_found->port,\n                                               track_id,\n                                               inputs_found->channel);\n\n                inputs_found++;\n            }\n\n            auto output_connections = get_all_kb_output_connections();\n            auto outputs_found = std::find_if(output_connections.begin(),\n                                              output_connections.end(),\n                                              [&](const auto& connection)\n                                              {\n                                                  return connection.track_id == track_id;\n                                              });\n\n            while (outputs_found != output_connections.end())\n            {\n                disconnect_track_from_output(outputs_found->port,\n                                             track_id,\n                                             outputs_found->channel);\n                outputs_found++;\n            }\n\n            ELKLOG_LOG_DEBUG(\"MidiController received a TRACK_DELETED notification for track {}\", event->track());\n            break;\n        }\n        default:\n            break;\n    }\n\n    return EventStatus::HANDLED_OK;\n}\n\nbool MidiDispatcher::_handle_engine_notification(const EngineNotificationEvent* event)\n{\n    if (event->is_audio_graph_notification())\n    {\n        return _handle_audio_graph_notification(static_cast<const AudioGraphNotificationEvent*>(event));\n    }\n    else if (event->is_playing_mode_notification())\n    {\n        return _handle_transport_notification(static_cast<const PlayingModeNotificationEvent*>(event));\n    }\n    else if (event->is_timing_tick_notification())\n    {\n        return _handle_tick_notification(static_cast<const EngineTimingTickNotificationEvent*>(event));\n    }\n    return false;\n}\n\nbool MidiDispatcher::_handle_transport_notification(const PlayingModeNotificationEvent* event)\n{\n    switch (event->mode())\n    {\n        case PlayingMode::PLAYING:\n            for (int i = 0; i < _midi_outputs; ++i)\n            {\n                if (_enabled_clock_out[i])\n                {\n                    ELKLOG_LOG_DEBUG(\"Sending midi start message\");\n                    _frontend->send_midi(i, midi::encode_start_message(), event->time());\n                }\n            }\n            break;\n\n        case PlayingMode::STOPPED:\n            for (int i = 0; i < _midi_outputs; ++i)\n            {\n                if (_enabled_clock_out[i])\n                {\n                    ELKLOG_LOG_DEBUG(\"Sending midi stop message\");\n                    _frontend->send_midi(i, midi::encode_stop_message(), event->time());\n                }\n            }\n            break;\n\n        default:\n            break;\n    }\n    return EventStatus::HANDLED_OK;\n}\n\nbool MidiDispatcher::_handle_tick_notification(const EngineTimingTickNotificationEvent* event)\n{\n    for (int i = 0; i < _midi_outputs; ++i)\n    {\n        if (_enabled_clock_out[i])\n        {\n            _frontend->send_midi(i, midi::encode_timing_clock(), event->time());\n        }\n    }\n    return EventStatus::HANDLED_OK;\n}\n\n} // end namespace sushi::internal::midi_dispatcher\n"
  },
  {
    "path": "src/engine/midi_dispatcher.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Handles translation of midi to internal events and midi routing\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_MIDI_DISPATCHER_H\n#define SUSHI_MIDI_DISPATCHER_H\n\n#include <string>\n#include <map>\n#include <array>\n#include <vector>\n#include <mutex>\n\n#include \"sushi/constants.h\"\n#include \"sushi/types.h\"\n\n#include \"library/midi_decoder.h\"\n#include \"library/event.h\"\n#include \"library/processor.h\"\n#include \"control_frontends/base_midi_frontend.h\"\n#include \"library/event_interface.h\"\n\nnamespace sushi::internal {\n\nnamespace engine {\nclass BaseProcessorContainer;\n}\n\nnamespace midi_dispatcher {\n\nstruct InputConnection\n{\n    // TODO: This can be track_id, if the InputConnection is member of KbdInputConnection.\n    //   It can also be processor_id, if the InputConnection is member of CCInputConnection.\n    //   Disambiguating would be safer (AUD-565).\n    ObjectId target;\n    ObjectId parameter;\n    float min_range;\n    float max_range;\n    bool relative;\n    uint8_t virtual_abs_value;\n};\n\nstruct OutputConnection\n{\n    int channel;\n    int output;\n    int cc_number;\n    float min_range;\n    float max_range;\n};\n\nenum class MidiDispatcherStatus\n{\n    OK,\n    INVALID_MIDI_INPUT,\n    INVALID_MIDI_OUTPUT,\n    INVALID_PROCESSOR,\n    INVALID_TRACK,\n    INVALID_PARAMETER,\n    INVAlID_CHANNEL\n};\n\n// These structs are only used for returning query data, to midi_controller.\nstruct CCInputConnection\n{\n    InputConnection input_connection;\n    int channel;\n    int port;\n    int cc;\n};\n\nstruct PCInputConnection\n{\n    ObjectId processor_id;\n    int channel;\n    int port;\n};\n\nstruct KbdInputConnection\n{\n    InputConnection input_connection;\n    int port;\n    int channel;\n    bool raw_midi;\n};\n\nstruct KbdOutputConnection\n{\n    ObjectId track_id;\n    int port;\n    int channel;\n};\n\nclass Accessor;\n\nclass MidiDispatcher : public EventPoster, public midi_receiver::MidiReceiver\n{\n    SUSHI_DECLARE_NON_COPYABLE(MidiDispatcher);\n\npublic:\n    explicit MidiDispatcher(dispatcher::BaseEventDispatcher* event_dispatcher);\n\n    ~MidiDispatcher() override;\n\n    void set_frontend(midi_frontend::BaseMidiFrontend* frontend)\n    {\n        _frontend = frontend;\n    }\n\n    /**\n     * @brief Sets the number of midi input ports.\n     * Not intended to be called dynamically, only once during creation.\n     * @param ports number of input ports.\n     */\n    void set_midi_inputs(int no_inputs)\n    {\n        _midi_inputs = no_inputs;\n    }\n\n    /**\n     * @brief Returns the number of midi input ports.\n     */\n    [[nodiscard]] int get_midi_inputs() const\n    {\n        return _midi_inputs;\n    }\n\n    /**\n     * @brief Sets the number of midi output ports.\n     * Not intended to be called dynamically, only once during creation.\n     * @param ports number of output ports.\n     */\n    void set_midi_outputs(int no_outputs);\n\n    /**\n     * @brief Returns the number of midi output ports.\n     */\n    [[nodiscard]] int get_midi_outputs() const\n    {\n        return _midi_outputs;\n    }\n\n    /**\n     * @brief Connects a midi control change message to a given parameter.\n     *        Eventually you should be able to set range, curve etc here.\n     * @param midi_input Index to the registered midi output.\n     * @param processor The processor target\n     * @param parameter The parameter to map to\n     * @param cc_no The cc id to use\n     * @param min_range Minimum range for this controller\n     * @param max_range Maximum range for this controller\n     * @param channel If not OMNI, only the given channel will be connected.\n     * @return OK if successfully forwarded midi message\n     */\n    MidiDispatcherStatus connect_cc_to_parameter(int midi_input,\n                                                 ObjectId processor_id,\n                                                 ObjectId parameter_id,\n                                                 int cc_no,\n                                                 float min_range,\n                                                 float max_range,\n                                                 bool use_relative_mode,\n                                                 int channel = midi::MidiChannel::OMNI);\n\n    /**\n     * @brief Disconnects a midi control change message from a given parameter.\n     * @param midi_input Index to the registered midi output.\n     * @param processor_id The processor target\n     * @param parameter The parameter mapped\n     * @param cc_no The cc id to use\n     * @return OK if successfully disconnected\n     */\n    MidiDispatcherStatus disconnect_cc_from_parameter(int midi_input,\n                                                      ObjectId processor_id,\n                                                      int cc_no,\n                                                      int channel = midi::MidiChannel::OMNI);\n\n    /**\n     * @brief Disconnects all midi control change messages from a given processor's parameters.\n     * @param processor_id The processor target\n     * @return OK if successfully disconnected\n     */\n    MidiDispatcherStatus disconnect_all_cc_from_processor(ObjectId processor_id);\n\n    /**\n     * @brief Returns a vector of CC_InputConnections for all the Midi Control Change input connections defined.\n     * @return A vector of CC_InputConnections.\n     */\n    std::vector<CCInputConnection> get_all_cc_input_connections();\n\n    /**\n     * @brief Returns a vector of CC_InputConnections for all the Midi Control Change input connections\n     * defined for the processor id passed as input.\n     * @param The id of the processor for which the connections are queried.\n     * @return A vector of CC_InputConnections.\n     */\n    std::vector<CCInputConnection> get_cc_input_connections_for_processor(int processor_id);\n\n    /**\n     * @brief Connects midi program change messages to a processor.\n     * @param midi_input Index to the registered midi output.\n     * @param processor The processor target\n     * @param channel If not OMNI, only the given channel will be connected.\n     * @return OK if successfully forwarded midi message\n     */\n    MidiDispatcherStatus connect_pc_to_processor(int midi_input,\n                                                 ObjectId processor_id,\n                                                 int channel = midi::MidiChannel::OMNI);\n\n    /**\n     * @brief Disconnects midi program change messages from a processor.\n     * @param midi_input Index to the registered midi output.\n     * @param processor The processor target\n     * @return OK if successfully disconnected\n     */\n    MidiDispatcherStatus disconnect_pc_from_processor(int midi_input,\n                                                      ObjectId processor_id,\n                                                      int channel = midi::MidiChannel::OMNI);\n\n    /**\n     * @brief Disconnects all midi program change messages from a given processor.\n     * @param processor_id The processor target\n     * @return OK if successfully disconnected\n     */\n    MidiDispatcherStatus disconnect_all_pc_from_processor(ObjectId processor_id);\n\n    /**\n     * @brief Returns a vector of PC_InputConnections for all the Midi Program Change input connections defined.\n     * @return A vector of PC_InputConnections.\n     */\n    std::vector<PCInputConnection> get_all_pc_input_connections();\n\n    /**\n     * @brief Returns a vector of PC_InputConnections for all the Midi Program Change input connections\n     * defined for the processor id passed as input.\n     * @param The id of the processor for which the connections are queried.\n     * @return A vector of PC_InputConnections.\n     */\n    std::vector<PCInputConnection> get_pc_input_connections_for_processor(int processor_id);\n\n    /**\n     * @brief Connect a midi input to a track\n     *        Possibly filtering on midi channel.\n     * @param midi_input Index of the midi input\n     * @param track_name The track/processor track to send to\n     * @param channel If not OMNI, only the given channel will be connected.\n     * @return OK if successfully connected the track, error status otherwise\n     */\n    MidiDispatcherStatus connect_kb_to_track(int midi_input,\n                                             ObjectId track_id,\n                                             int channel = midi::MidiChannel::OMNI);\n\n    /**\n     * @brief Disconnect a midi input from a track\n     * @param midi_input Index of the midi input\n     * @param track_name The track/processor track\n     * @param channel If not OMNI, only the given channel will be connected.\n     * @return OK if successfully disconnected from the track, error status otherwise\n     */\n    MidiDispatcherStatus disconnect_kb_from_track(int midi_input,\n                                                  ObjectId track_id,\n                                                  int channel = midi::MidiChannel::OMNI);\n\n    /**\n     * @brief Returns a vector of Kbd_InputConnections for all the Midi Keyboard input connections defined.\n     * @return A vector of Kbd_InputConnections.\n     */\n    std::vector<KbdInputConnection> get_all_kb_input_connections();\n\n    /**\n     * @brief Connect a midi input to a track and send unprocessed\n     *        Midi data to it. Possibly filtering on midi channel.\n     * @param midi_input Index of the midi input\n     * @param track_name The track/processor track to send to\n     * @param channel If not OMNI, only the given channel will be connected.\n     * @return OK if successfully connected the track, error status otherwise\n     */\n    MidiDispatcherStatus connect_raw_midi_to_track(int midi_input,\n                                                   ObjectId track_id,\n                                                   int channel = midi::MidiChannel::OMNI);\n\n    /**\n     * @brief Disconnect a midi input from a track.\n     * @param midi_input Index of the midi input\n     * @param track_name The track/processor track to disconnect\n     * @return OK if successfully disconnected the track, error status otherwise\n     */\n    MidiDispatcherStatus disconnect_raw_midi_from_track(int midi_input,\n                                                        ObjectId track_id,\n                                                        int channel = midi::MidiChannel::OMNI);\n\n    /**\n     * @brief Connect midi kb data from a track to a given midi output\n     * @param midi_output Index of the midi out\n     * @param track_name The track/processor track from where the data originates\n     * @param channel Which channel nr to output the data on\n     * @return OK if successfully connected the track, error status otherwise\n     */\n    MidiDispatcherStatus connect_track_to_output(int midi_output,\n                                                 ObjectId track_id,\n                                                 int channel);\n\n    /**\n     * @brief Disconnect midi kb data from a track to a given midi output\n     * @param midi_output Index of the midi out\n     * @param track_name The track/processor track from where the data originates\n     * @return OK if successfully disconnected the track, error status otherwise\n     */\n    MidiDispatcherStatus disconnect_track_from_output(int midi_output,\n                                                      ObjectId track_id,\n                                                      int channel);\n\n    /**\n     * @brief Returns a vector of Kbd_OutputConnections for all the Midi Keyboard output connections defined.\n     * @return A vector of Kbd_OutputConnections.\n     */\n    std::vector<KbdOutputConnection> get_all_kb_output_connections();\n\n    /**\n     * @brief Enable or disable sending of midi clock through an output\n     * @param enabled If true enables sending of midi clock messages (24ppqn),\n     *        and start and stop messages.\n     * @param midi_output The midi output to configure\n     * @return\n     */\n    MidiDispatcherStatus enable_midi_clock(bool enabled, int midi_output);\n\n    /**\n     * @brief Returns whether midi clock output is enabled for a particular midi output\n     * @param midi_output The midi output port\n     * @return true if the selected midi output is configured to send midi clock, false otherwise\n     */\n    bool midi_clock_enabled(int midi_output);\n\n    /**\n     * @brief Process a raw midi message and send it of according to the\n     *        configured connections.\n     * @param port Index of the originating midi port.\n     * @param data Pointer to the raw midi message.\n     * @param size Length of data in bytes.\n     * @param timestamp timestamp of the midi event\n     */\n    void send_midi(int port, MidiDataByte data, Time timestamp) override;\n\n    /* Inherited from EventPoster */\n    int process(Event* /*event*/) override;\n\nprivate:\n    friend Accessor;\n\n    bool _handle_engine_notification(const EngineNotificationEvent* event);\n    bool _handle_audio_graph_notification(const AudioGraphNotificationEvent* event);\n    bool _handle_transport_notification(const PlayingModeNotificationEvent* event);\n    bool _handle_tick_notification(const EngineTimingTickNotificationEvent* event);\n\n    std::vector<CCInputConnection> _get_cc_input_connections(std::optional<int> processor_id_filter);\n    std::vector<PCInputConnection> _get_pc_input_connections(std::optional<int> processor_id_filter);\n\n    using InputConnections = std::array<std::vector<InputConnection>, midi::MidiChannel::OMNI + 1>;\n\n    using KeyboardRoutesIn = std::map<int, InputConnections>;\n    using KeyboardRoutesOut = std::map<ObjectId, std::vector<OutputConnection>>;\n    using CcRoutes = std::map<int, std::array<InputConnections, midi::MAX_CONTROLLER_NO + 1>>;\n    using PcRoutes = std::map<int, InputConnections>;\n    using RawRoutesIn = std::map<int, InputConnections>;\n\n    KeyboardRoutesIn _kb_routes_in;\n    KeyboardRoutesOut _kb_routes_out;\n    CcRoutes _cc_routes;\n    PcRoutes _pc_routes;\n    RawRoutesIn _raw_routes_in;\n\n    int _midi_inputs {0};\n    int _midi_outputs {0};\n\n    std::mutex _kb_routes_in_lock;\n    std::mutex _kb_routes_out_lock;\n    std::mutex _cc_routes_lock;\n    std::mutex _pc_routes_lock;\n    std::mutex _raw_routes_in_lock;\n\n    std::vector<int> _enabled_clock_out;\n\n    midi_frontend::BaseMidiFrontend* _frontend;\n    dispatcher::BaseEventDispatcher* _event_dispatcher;\n};\n\n} // end namespace midi_dispatcher\n} // end namespace sushi::internal\n\n#endif // SUSHI_MIDI_DISPATCHER_H\n"
  },
  {
    "path": "src/engine/midi_receiver.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Interface class for receiving midi data\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_MIDI_RECEIVER_H\n#define SUSHI_MIDI_RECEIVER_H\n\n#include \"sushi/sushi_time.h\"\n\nnamespace sushi::internal::midi_receiver {\n\nclass MidiReceiver\n{\npublic:\n    virtual ~MidiReceiver() = default;\n\n    virtual void send_midi(int port, MidiDataByte data, Time timestamp) = 0;\n};\n\n\n} // end namespace sushi::internal::midi_dispatcher\n\n#endif // SUSHI_MIDI_RECEIVER_H\n\n"
  },
  {
    "path": "src/engine/parameter_manager.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Class to manage parameter changes, rate limiting and sync between devices\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"parameter_manager.h\"\n#include \"library/processor.h\"\n#include \"engine/base_processor_container.h\"\n\nnamespace sushi::internal {\n\ninline void send_parameter_notification(ObjectId processor_id,\n                                        ObjectId parameter_id,\n                                        float normalized_value,\n                                        float domain_value,\n                                        const std::string& formatted_value,\n                                        dispatcher::BaseEventDispatcher* dispatcher)\n{\n    auto event = std::make_unique<ParameterChangeNotificationEvent>(processor_id,\n                                                                    parameter_id,\n                                                                    normalized_value,\n                                                                    domain_value,\n                                                                    formatted_value,\n                                                                    IMMEDIATE_PROCESS);\n    dispatcher->dispatch(std::move(event));\n}\n\nParameterManager::ParameterManager(Time update_rate,\n                                   const engine::BaseProcessorContainer* processor_container) : _processors(processor_container),\n                                                                                                       _update_rate(update_rate)\n{}\n\nvoid ParameterManager::track_parameters(ObjectId processor_id)\n{\n    if (auto processor = _processors->processor(processor_id))\n    {\n        auto& param_map = _parameters[processor->id()];\n\n        for (const auto& p: processor->all_parameters())\n        {\n            auto type = p->type();\n            if (type == ParameterType::BOOL || type == ParameterType::INT || type == ParameterType::FLOAT)\n            {\n                param_map.insert({p->id(), {.value = processor->parameter_value(p->id()).second, .last_update = Time(0)}});\n            }\n        }\n    }\n}\n\nvoid ParameterManager::untrack_parameters(ObjectId processor_id)\n{\n    _parameters.erase(processor_id);\n}\n\nvoid ParameterManager::mark_parameter_changed(ObjectId processor_id, ObjectId parameter_id, Time timestamp)\n{\n    _parameter_change_queue.push_back(ParameterUpdate{processor_id, parameter_id, timestamp});\n}\n\nvoid ParameterManager::mark_processor_changed(ObjectId processor_id, Time timestamp)\n{\n    auto entry = std::find_if(_processor_change_queue.begin(),\n                              _processor_change_queue.end(),\n                              [&](const auto& i){return i.processor_id == processor_id;});\n    if (entry == _processor_change_queue.end())\n    {\n        _processor_change_queue.push_back(ProcessorUpdate{processor_id, timestamp});\n    }\n    else\n    {\n        entry->update_time = timestamp;\n    }\n}\n\nvoid ParameterManager::output_parameter_notifications(dispatcher::BaseEventDispatcher* dispatcher, Time target_time)\n{\n    _output_processor_notifications(dispatcher, target_time);\n    _output_parameter_notifications(dispatcher, target_time);\n}\n\nvoid ParameterManager::_output_parameter_notifications(dispatcher::BaseEventDispatcher* dispatcher, Time timestamp)\n{\n    auto i = _parameter_change_queue.begin();\n    auto swap_iter = i;\n    while (i != _parameter_change_queue.end())\n    {\n        if (auto proc_node = _parameters.find(i->processor_id); proc_node != _parameters.end())\n        {\n            auto& param_entries = proc_node->second;\n\n            if (const auto& param_node = param_entries.find(i->parameter_id); param_node != param_entries.end())\n            {\n                auto& param_entry = param_node->second;\n                /* Send update if the update time has passed and the last update was sent\n                 * longer than _update_rate ago */\n                if (i->update_time <= timestamp && (param_entry.last_update + _update_rate) <= timestamp)\n                {\n                    if (auto processor = _processors->processor(i->processor_id))\n                    {\n                        float value = processor->parameter_value(i->parameter_id).second;\n                        if (value != param_entry.value)\n                        {\n                            send_parameter_notification(i->processor_id, i->parameter_id, value,\n                                                        processor->parameter_value_in_domain(i->parameter_id).second,\n                                                        processor->parameter_value_formatted(i->parameter_id).second,\n                                                        dispatcher);\n                            param_entry.last_update = timestamp;\n                            param_entry.value = value;\n                        }\n                    }\n                }\n                    /* If this parameter was not a duplicate, but still updated too recently,\n                     * put it at the front of the queue and check next time */\n                else if (param_entry.last_update != timestamp)\n                {\n                    std::iter_swap(i, swap_iter);\n                    swap_iter++;\n                }\n            }\n        }\n        i++;\n    }\n    _parameter_change_queue.erase(swap_iter, _parameter_change_queue.end());\n}\n\nvoid ParameterManager::_output_processor_notifications(dispatcher::BaseEventDispatcher* dispatcher, Time timestamp)\n{\n    auto i = _processor_change_queue.begin();\n    auto swap_iter = i;\n    while (i != _processor_change_queue.end())\n    {\n        /* When notifying all parameters of a processor, we ignore the last_update time\n         * and send a notification anyway, regardless if one was sent recently */\n        if (i->update_time <= timestamp)\n        {\n            if (auto processor = _processors->processor(i->processor_id))\n            {\n                auto& param_entries = _parameters[i->processor_id];\n                for (auto& p: param_entries)\n                {\n                    auto& entry = p.second;\n                    float value = processor->parameter_value(p.first).second;\n                    if (value != entry.value)\n                    {\n                        send_parameter_notification(i->processor_id, p.first, value,\n                                                    processor->parameter_value_in_domain(p.first).second,\n                                                    processor->parameter_value_formatted(p.first).second, dispatcher);\n                        entry.value = value;\n                        entry.last_update = timestamp;\n                    }\n                }\n            }\n        }\n        else\n        {\n            std::iter_swap(i, swap_iter);\n            swap_iter++;\n        }\n        i++;\n    }\n    _processor_change_queue.erase(swap_iter, _processor_change_queue.end());\n}\n\nbool ParameterManager::parameter_change_queue_empty() const\n{\n    return _parameter_change_queue.empty();\n}\n\n} // end namespace sushi::internal"
  },
  {
    "path": "src/engine/parameter_manager.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Class to manage parameter changes, rate limiting and sync between devices\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n \n#ifndef SUSHI_PARAMETER_MANAGER_H\n#define SUSHI_PARAMETER_MANAGER_H\n\n#include <unordered_map>\n#include <vector>\n\n#include \"sushi/constants.h\"\n#include \"sushi/types.h\"\n\n#include \"library/plugin_parameters.h\"\n#include \"library/event_interface.h\"\n#include \"library/event.h\"\n\nnamespace sushi::internal {\n\nnamespace engine {class BaseProcessorContainer;}\nnamespace dispatcher {class BaseEventDispatcher;}\n\nclass ParameterManagerAccessor;\n\nclass ParameterManager\n{\npublic:\n    /**\n     * @brief Construct a ParameterManager object\n     * @param update_rate The minimum time between 2 consecutive updates for a specific parameter\n     * @param processor_container A ProcessorContainer object used to retrieve processors\n     */\n    ParameterManager(Time update_rate, const engine::BaseProcessorContainer* processor_container);\n\n    /**\n     * @brief Add a processor whose parameter values should be tracked\n     * @param processor_id The id of the processor\n     */\n    void track_parameters(ObjectId processor_id);\n\n    /**\n     * @brief Remove all tracked parameters of a processor\n     * @param processor_id The id of the Processor\n     */\n    void untrack_parameters(ObjectId processor_id);\n\n    /**\n     * @brief Mark a parameter as changed and queue a value update\n     * @param processor_id The id of the Processor\n     * @param parameter_id The id of the parameter\n     * @param timestamp The time at which the value changed, can be in the future\n     */\n    void mark_parameter_changed(ObjectId processor_id, ObjectId parameter_id, Time timestamp);\n\n    /**\n     * @brief Mark all parameters of a processor as changed and queue updates for all parameters\n     * @param processor_id The id of the Processor\n     * @param timestamp The time at which the values changed, can be in the future\n     */\n    void mark_processor_changed(ObjectId processor_id, Time timestamp);\n\n    /**\n     * @brief Output ParameterChangedNotificationEvents for all queued parameter changes up until\n     *        a given timestamp. If a parameter was queued several time, only one notification\n     *        will be sent.\n     * @param dispatcher The dispatcher to send events to.\n     * @param target_time All queued updates with a timestamp equal to or lower that this will be processed\n     */\n    void output_parameter_notifications(dispatcher::BaseEventDispatcher* dispatcher, Time target_time);\n\n    bool parameter_change_queue_empty() const;\n\nprivate:\n    friend ParameterManagerAccessor;\n\n    void _output_parameter_notifications(dispatcher::BaseEventDispatcher* dispatcher, Time timestamp);\n\n    void _output_processor_notifications(dispatcher::BaseEventDispatcher* dispatcher, Time timestamp);\n\n    struct ParameterEntry\n    {\n        float value;\n        Time last_update;\n    };\n\n    struct ParameterUpdate\n    {\n        ObjectId processor_id;\n        ObjectId parameter_id;\n        Time update_time;\n    };\n\n    struct ProcessorUpdate\n    {\n        ObjectId processor_id;\n        Time update_time;\n    };\n\n    using Parameters = std::unordered_map<ObjectId, std::unordered_map<ObjectId, ParameterEntry>>;\n\n    std::vector<ProcessorUpdate> _processor_change_queue;\n    std::vector<ParameterUpdate> _parameter_change_queue;\n\n    const engine::BaseProcessorContainer* _processors;\n    Time _update_rate;\n\n    // Note this is only accessed from the event loop thread, so no mutex is needed\n    Parameters _parameters;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_PARAMETER_MANAGER_H\n"
  },
  {
    "path": "src/engine/plugin_library.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"plugin_library.h\"\n\n#include <filesystem>\n\nnamespace sushi::internal::engine {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"plugin_library\");\n\nvoid PluginLibrary::set_base_plugin_path(const std::string& path)\n{\n    _base_plugin_path = path;\n    ELKLOG_LOG_WARNING_IF(_base_plugin_path != path,\n                         \"Overriding previously defined base plugin path: {} with: {}\",\n                         _base_plugin_path, path);\n    ELKLOG_LOG_INFO(\"Setting base plugin path to: {}\", _base_plugin_path);\n}\n\nstd::string PluginLibrary::to_absolute_path(const std::string& path)\n{\n    auto fspath = std::filesystem::path(path);\n    if (fspath.is_absolute() || path.empty())\n    {\n        return path;\n    }\n\n    auto full_path = std::filesystem::path(_base_plugin_path) / fspath;\n    return std::string(std::filesystem::absolute(full_path).string());\n}\n\n} // end namespace sushi::internal::engine\n\n"
  },
  {
    "path": "src/engine/plugin_library.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n * @Brief Interface used to handle a library of plugins in the target system.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_PLUGIN_LIBRARY_H\n#define SUSHI_PLUGIN_LIBRARY_H\n\n#include <string>\n\nnamespace sushi::internal::engine {\n\nclass PluginLibrary\n{\npublic:\n    PluginLibrary() = default;\n    virtual ~PluginLibrary() = default;\n\n    /**\n     * @brief Set an absolute path to be the base for plugin paths\n     *\n     * @param path Absolute path of the base plugin folder\n     */\n    void set_base_plugin_path(const std::string& path);\n\n    /**\n     * @brief Convert a relative plugin path to an absolute path,\n     *        if a base plugin path has been set.\n     *\n     * @param path Relative path to plugin inside the base plugin folder.\n     *             It is the caller's responsibility\n     *             to ensure that this is a proper relative path (not starting with \"/\").\n     *\n     * @return Absolute path of the plugin\n     */\n    std::string to_absolute_path(const std::string& path);\n\nprotected:\n    std::string _base_plugin_path;\n};\n\n} // end namespace sushi::internal::engine\n\n#endif // SUSHI_PLUGIN_LIBRARY_H\n"
  },
  {
    "path": "src/engine/processor_container.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Container for audio processors\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"processor_container.h\"\n\nnamespace sushi::internal::engine {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"engine\");\n\nbool ProcessorContainer::add_processor(std::shared_ptr<Processor> processor)\n{\n    std::scoped_lock<std::mutex, std::mutex> lock(_processors_by_name_lock, _processors_by_id_lock);\n    if (_processors_by_name.count(processor->name()) > 0)\n    {\n        return false;\n    }\n    _processors_by_name[processor->name()] = processor;\n    _processors_by_id[processor->id()] = processor;\n    return true;\n}\n\nbool ProcessorContainer::add_track(std::shared_ptr<Track> track)\n{\n    std::scoped_lock<std::mutex> lock(_processors_by_track_lock);\n    if (_processors_by_track.count(track->id()) > 0)\n    {\n        return false;\n    }\n    _processors_by_track[track->id()].clear();\n    return true;\n}\n\nbool ProcessorContainer::remove_processor(ObjectId id)\n{\n    auto processor = this->processor(id);\n    if (processor == nullptr)\n    {\n        return false;\n    }\n    {\n        std::scoped_lock<std::mutex> lock(_processors_by_id_lock);\n        [[maybe_unused]] auto count = _processors_by_id.erase(processor->id());\n        ELKLOG_LOG_WARNING_IF(count != 1, \"Erased {} instances of processor {}\", count, processor->name())\n    }\n    {\n        std::scoped_lock<std::mutex> lock(_processors_by_name_lock);\n        [[maybe_unused]] auto count = _processors_by_name.erase(processor->name());\n        ELKLOG_LOG_WARNING_IF(count != 1, \"Erased {} instances of processor {}\", count, processor->name())\n    }\n    return true;\n}\n\nbool ProcessorContainer::remove_track(ObjectId track_id)\n{\n    std::scoped_lock<std::mutex> lock(_processors_by_track_lock);\n    assert(_processors_by_track[track_id].empty());\n    _processors_by_track.erase(track_id);\n    return true;\n}\n\nbool ProcessorContainer::add_to_track(std::shared_ptr<Processor> processor, ObjectId track_id,\n                                      std::optional<ObjectId> before_id)\n{\n    std::scoped_lock<std::mutex> lock(_processors_by_track_lock);\n    auto& track_processors = _processors_by_track[track_id];\n\n    if (before_id.has_value())\n    {\n        for (auto i = track_processors.begin(); i != track_processors.end(); ++i)\n        {\n            if ((*i)->id() == before_id.value())\n            {\n                track_processors.insert(i, processor);\n                return true;\n            }\n        }\n        // If we end up here, the track's processing chain and _processors_by_track has diverged.\n        assert(false);\n    }\n    else\n    {\n        track_processors.push_back(processor);\n    }\n    return true;\n}\n\nbool ProcessorContainer::processor_exists(ObjectId id) const\n{\n    std::scoped_lock<std::mutex> lock(_processors_by_id_lock);\n    return _processors_by_id.count(id) > 0;\n}\n\nbool ProcessorContainer::processor_exists(const std::string& name) const\n{\n    std::scoped_lock<std::mutex> lock(_processors_by_name_lock);\n    return _processors_by_name.count(name) > 0;\n}\n\nbool ProcessorContainer::remove_from_track(ObjectId processor_id, ObjectId track_id)\n{\n    std::scoped_lock<std::mutex> lock(_processors_by_track_lock);\n    auto& track_processors = _processors_by_track[track_id];\n    for (auto i = track_processors.cbegin(); i != track_processors.cend(); ++i)\n    {\n        if ((*i)->id() == processor_id)\n        {\n            track_processors.erase(i);\n            return true;\n        }\n    }\n    return false;\n}\n\nstd::vector<std::shared_ptr<const Processor>> ProcessorContainer::all_processors() const\n{\n    std::vector<std::shared_ptr<const Processor>> processors;\n    std::scoped_lock<std::mutex> lock(_processors_by_id_lock);\n    processors.reserve(_processors_by_id.size());\n    for (const auto& p : _processors_by_id)\n    {\n        processors.emplace_back(p.second);\n    }\n    return processors;\n}\n\nstd::shared_ptr<Processor> ProcessorContainer::mutable_processor(ObjectId id) const\n{\n    return std::const_pointer_cast<Processor>(this->processor(id));\n}\n\nstd::shared_ptr<Processor> ProcessorContainer::mutable_processor(const std::string& name) const\n{\n    return std::const_pointer_cast<Processor>(this->processor(name));\n}\n\nstd::shared_ptr<const Processor> ProcessorContainer::processor(ObjectId id) const\n{\n    std::scoped_lock<std::mutex> lock(_processors_by_id_lock);\n    auto processor_node = _processors_by_id.find(id);\n    if (processor_node == _processors_by_id.end())\n    {\n        return nullptr;\n    }\n    return processor_node->second;\n}\n\nstd::shared_ptr<const Processor> ProcessorContainer::processor(const std::string& name) const\n{\n    std::scoped_lock<std::mutex> lock(_processors_by_name_lock);\n    auto processor_node = _processors_by_name.find(name);\n    if (processor_node == _processors_by_name.end())\n    {\n        return nullptr;\n    }\n    return processor_node->second;\n}\n\nstd::shared_ptr<Track> ProcessorContainer::mutable_track(ObjectId track_id) const\n{\n    return std::const_pointer_cast<Track>(this->track(track_id));\n}\n\nstd::shared_ptr<Track> ProcessorContainer::mutable_track(const std::string& track_name) const\n{\n    return std::const_pointer_cast<Track>(this->track(track_name));\n}\n\nstd::shared_ptr<const Track> ProcessorContainer::track(ObjectId track_id) const\n{\n    /* Check if there is an entry for the ObjectId in the list of track processor\n     * In that case we can safely look up the processor by its id and cast it */\n    std::scoped_lock<std::mutex> lock(_processors_by_track_lock);\n    if (_processors_by_track.count(track_id) > 0)\n    {\n        std::scoped_lock<std::mutex> id_lock(_processors_by_id_lock);\n        auto track_node = _processors_by_id.find(track_id);\n        if (track_node != _processors_by_id.end())\n        {\n            return std::static_pointer_cast<const Track>(track_node->second);\n        }\n    }\n    return nullptr;\n}\n\nstd::shared_ptr<const Track> ProcessorContainer::track(const std::string& track_name) const\n{\n    std::scoped_lock<std::mutex> _lock(_processors_by_name_lock);\n    auto track_node = _processors_by_name.find(track_name);\n    if (track_node != _processors_by_name.end())\n    {\n        /* Check if there is an entry for the ObjectId in the list of track processor\n         * In that case we can safely look up the processor by its id and cast it */\n        std::scoped_lock<std::mutex> lock(_processors_by_track_lock);\n        if (_processors_by_track.count(track_node->second->id()) > 0)\n        {\n            return std::static_pointer_cast<const Track>(track_node->second);\n        }\n    }\n    return nullptr;\n}\n\nstd::vector<std::shared_ptr<const Processor>> ProcessorContainer::processors_on_track(ObjectId track_id) const\n{\n    std::vector<std::shared_ptr<const Processor>> processors;\n    std::scoped_lock<std::mutex> lock(_processors_by_track_lock);\n    auto track_node = _processors_by_track.find(track_id);\n    if (track_node != _processors_by_track.end())\n    {\n        processors.reserve(track_node->second.size());\n        for (const auto& p : track_node->second)\n        {\n            processors.push_back(p);\n        }\n    }\n    return processors;\n}\n\nstd::vector<std::shared_ptr<const Track>> ProcessorContainer::all_tracks() const\n{\n    std::vector<std::shared_ptr<const Track>> tracks;\n    {\n        std::scoped_lock<std::mutex, std::mutex> lock(_processors_by_track_lock, _processors_by_id_lock);\n        for (const auto& p : _processors_by_track)\n        {\n            auto processor_node = _processors_by_id.find(p.first);\n            if (processor_node != _processors_by_id.end())\n            {\n                tracks.push_back(std::static_pointer_cast<const Track, Processor>(processor_node->second));\n            }\n        }\n    }\n    /* Sort the list so tracks are listed in the order they were created */\n    std::sort(tracks.begin(), tracks.end(), [](auto a, auto b) {return a->id() < b->id();});\n    return tracks;\n}\n\n} // end namespace sushi::internal::engine"
  },
  {
    "path": "src/engine/processor_container.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Container for audio processors\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_PROCESSOR_CONTAINER_H\n#define SUSHI_PROCESSOR_CONTAINER_H\n\n#include <memory>\n#include <unordered_map>\n#include <vector>\n#include <utility>\n#include <mutex>\n\n#include \"base_processor_container.h\"\n#include \"track.h\"\n#include \"library/processor.h\"\n\nnamespace sushi::internal::engine {\n\nclass ProcessorContainer : public BaseProcessorContainer\n{\npublic:\n    ProcessorContainer() = default;\n    SUSHI_DECLARE_NON_COPYABLE(ProcessorContainer);\n\n    /**\n     * @brief Add a processor to the container\n     * @param processor The Processor instance to add\n     * @return true if the track was successfully added, false otherwise.\n     */\n    bool add_processor(std::shared_ptr<Processor> processor) override;\n\n    /**\n     * @brief Add a track to the container\n     * @param track The Track instance to add\n     * @return true if the track was successfully added, false otherwise.\n     */\n    bool add_track(std::shared_ptr<Track> track) override;\n\n    /**\n     * @brief Remove a processor from the container\n     * @param id The id of the processor\n     * @return true if the processor was found and successfully removed,\n     *         false otherwise\n     */\n    bool remove_processor(ObjectId id) override;\n\n    /**\n     * @brief Remove a track\n     * @param track_id The id of the track\n     * @return true of the track was found and successfully removed,\n     *         false otherwise\n     */\n    bool remove_track(ObjectId track_id) override;\n\n    /**\n     * @brief Add a processor from a track entry\n     * @param processor The processor instance to add.\n     * @param track_id The id of the track to add to\n     * @param before_id If populated, the processor will be added before the\n     *        processor with the give id, otherwise the processor will be\n     *        added to the back of the list of processors\n     * @return\n     */\n    bool add_to_track(std::shared_ptr<Processor> processor, ObjectId track_id, std::optional<ObjectId> before_id) override;\n\n    /**\n     * @brief Remove a processor from a track entry\n     * @param processor_id The id of the processor to remove.\n     * @param track_id  The id of the track to remove from\n     * @return true if the processor was successfully removed from the track,\n     *         false otherwise\n     */\n    bool remove_from_track(ObjectId processor_id, ObjectId track_id) override;\n\n    /**\n    * @brief Query whether a particular processor exists in the container\n    * @param name The unique id of the processor\n    * @return true if the processor exists, false otherwise\n    */\n    bool processor_exists(ObjectId id) const override;\n\n    /**\n     * @brief Query whether a particular processor exists in the container\n     * @param name The unique name of the processor\n     * @return true if the processor exists, false otherwise\n     */\n    bool processor_exists(const std::string& name) const override;\n\n    /**\n     * @brief Return all processors.\n     * @return An std::vector containing all registered processors.\n     */\n    std::vector<std::shared_ptr<const Processor>> all_processors() const override;\n\n    /**\n     * @brief Access a particular processor by its unique id for editing,\n     *        use with care and not from several threads at once\n     * @param processor_id The id of the processor\n     * @return A mutable pointer to the processor instance if found, nullptr otherwise\n     */\n    std::shared_ptr<Processor> mutable_processor(ObjectId id) const override;\n\n    /**\n     * @brief Access a particular processor by its unique name for editing\n     * @param processor_name The name of the processor\n     * @return A std::shared_ptr<Processor> to the processor instance if found,\n     *         nullptr otherwise\n     */\n    std::shared_ptr<Processor> mutable_processor(const std::string& name) const override;\n\n    /**\n     * @brief Access a particular processor by its unique id for querying\n     * @param processor_id The id of the processor\n     * @return A std::shared_ptr<const Processor> to the processor instance if found,\n     *         nullptr otherwise\n     */\n    std::shared_ptr<const Processor> processor(ObjectId) const override;\n\n    /**\n     * @brief Access a particular processor by its unique name for querying\n     * @param processor_name The name of the processor\n     * @return A std::shared_ptr<const Processor> to the processor instance if found,\n     *         nullptr otherwise\n     */\n    std::shared_ptr<const Processor> processor(const std::string& name) const override;\n\n    /**\n     * @brief Access a particular track by its unique id for editing\n     * @param track_id The id of the track\n     * @return A std::shared_ptr<Track> to the track instance if found,\n     *         nullptr otherwise\n     */\n    std::shared_ptr<Track> mutable_track(ObjectId track_id) const override;\n\n    /**\n     * @brief Access a particular track by its unique name for editing\n     * @param track_name The name of the track\n     * @return A std::shared_ptr<Track> to the track instance if found,\n     *         nullptr otherwise\n     */\n    std::shared_ptr<Track> mutable_track(const std::string& track_name) const override;\n\n    /**\n     * @brief Access a particular track by its unique id for querying\n     * @param track_id The id of the track\n     * @return A std::shared_ptr<const Track> to the track instance if found,\n     *         nullptr otherwise\n     */\n    std::shared_ptr<const Track> track(ObjectId track_id) const override;\n\n    /**\n     * @brief Access a particular track by its unique name for querying\n     * @param track_name The name of the track\n     * @return A std::shared_ptr<const Track> to the track instance if found,\n     *         nullptr otherwise\n     */\n    std::shared_ptr<const Track> track(const std::string& name) const override;\n\n    /**\n     * @brief Return all processors on a given Track.\n     * @param track_id The id of the track\n     * @return An std::vector containing all processors on a track in order of processing.\n     */\n    std::vector<std::shared_ptr<const Processor>> processors_on_track(ObjectId track_id) const override;\n\n    /**\n     * @brief Return all tracks.\n     * @return An std::vector of containing all Tracks\n     */\n    std::vector<std::shared_ptr<const Track>> all_tracks() const override;\n\nprivate:\n    std::unordered_map<std::string, std::shared_ptr<Processor>>           _processors_by_name;\n    std::unordered_map<ObjectId, std::shared_ptr<Processor>>              _processors_by_id;\n    std::unordered_map<ObjectId, std::vector<std::shared_ptr<Processor>>> _processors_by_track;\n\n    mutable std::mutex _processors_by_name_lock;\n    mutable std::mutex _processors_by_id_lock;\n    mutable std::mutex _processors_by_track_lock;\n};\n\n} // end namespace sushi::internal::engine\n\n#endif // SUSHI_PROCESSOR_CONTAINER_H\n"
  },
  {
    "path": "src/engine/receiver.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Helper for asynchronous communication\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <thread>\n\n#include \"elklog/static_logger.h\"\n\n#include \"engine/receiver.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"event_receiver\");\n\nnamespace sushi::internal::receiver {\n\nconstexpr int MAX_RETRIES = 100;\n\nbool AsynchronousEventReceiver::wait_for_response(EventId id, std::chrono::milliseconds timeout)\n{\n    int retries = 0;\n    while (retries < MAX_RETRIES)\n    {\n        RtEvent event;\n        while (_queue->pop(event))\n        {\n            if (is_returnable_event(event))\n            {\n                auto typed_event = event.returnable_event();\n                if (typed_event->event_id() == id)\n                {\n                    auto handled_ok = typed_event->status() == ReturnableRtEvent::EventStatus::HANDLED_OK;\n                    ELKLOG_LOG_ERROR_IF(handled_ok == false, \"RtEvent with id {} returned with error\", id);\n                    return handled_ok;\n                }\n                bool status = (typed_event->status() == ReturnableRtEvent::EventStatus::HANDLED_OK);\n                _receive_list.push_back(Node{typed_event->event_id(), status});\n            }\n        }\n        for (auto i = _receive_list.begin(); i != _receive_list.end(); ++i)\n        {\n            if (i->id == id)\n            {\n                bool status = i->status;\n                _receive_list.erase(i);\n                return status;\n            }\n        }\n        std::this_thread::sleep_for(timeout / MAX_RETRIES);\n        retries++;\n    }\n    ELKLOG_LOG_WARNING(\"Waiting for RtEvent with id {} timed out\", id);\n    return false;\n}\n\n} // end namespace sushi::internal::receiver\n"
  },
  {
    "path": "src/engine/receiver.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Helper for asynchronous communication\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_ASYNCHRONOUS_RECEIVER_H\n#define SUSHI_ASYNCHRONOUS_RECEIVER_H\n\n#include <vector>\n#include <chrono>\n\n#include \"library/id_generator.h\"\n#include \"library/rt_event_fifo.h\"\n\nnamespace sushi::internal::receiver {\n\nclass AsynchronousEventReceiver\n{\npublic:\n    explicit AsynchronousEventReceiver(RtSafeRtEventFifo* queue) : _queue{queue} {}\n\n    /**\n     * @brief Blocks the current thread while waiting for a response to a given event\n     * @param id EventId of the event the thread is waiting for\n     * @param timeout Maximum wait time\n     * @return true if the event was received in time and handled properly, false otherwise\n     */\n    bool wait_for_response(EventId id, std::chrono::milliseconds timeout);\n\nprivate:\n    struct Node\n    {\n        EventId id;\n        bool    status;\n    };\n    std::vector<Node> _receive_list;\n    RtSafeRtEventFifo* _queue;\n};\n\n} // end namespace sushi::internal::receiver\n\n#endif // SUSHI_ASYNCHRONOUS_RECEIVER_H\n"
  },
  {
    "path": "src/engine/track.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Class to represent a mixer track with a chain of processors\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"track.h\"\n\nnamespace sushi::internal::engine {\n\nconstexpr int TRACK_MAX_PROCESSORS = 32;\nconstexpr float PAN_GAIN_3_DB = 1.412537f;\nconstexpr float DEFAULT_TRACK_GAIN = 1.0f;\n\n/* Map pan and gain to left and right gain with a 3 dB pan law */\ninline std::pair<float, float> calc_l_r_gain(float gain, float pan)\n{\n    float left_gain, right_gain;\n    if (pan < 0.0f) // Audio panned left\n    {\n        left_gain = gain * (1.0f + pan - PAN_GAIN_3_DB * pan);\n        right_gain = gain * (1.0f + pan);\n    }\n    else            // Audio panned right\n    {\n        left_gain = gain * (1.0f - pan);\n        right_gain = gain * (1.0f - pan + PAN_GAIN_3_DB * pan);\n    }\n    return {left_gain, right_gain};\n}\n\nTrack::Track(HostControl host_control,\n             int channels,\n             performance::PerformanceTimer* timer,\n             bool pan_controls,\n             TrackType type) : InternalPlugin(host_control),\n                                  _input_buffer{std::max(channels, 2)},\n                                  _output_buffer{std::max(channels, 2)},\n                                  _buses{1},\n                                  _type{type},\n                                  _timer{timer}\n{\n    _max_input_channels = channels;\n    _max_output_channels = std::max(channels, 2);\n    _current_input_channels = channels;\n    _current_output_channels = channels;\n    _common_init((pan_controls && channels <= 2) ? PanMode::PAN_AND_GAIN : PanMode::GAIN_ONLY);\n}\n\nTrack::Track(HostControl host_control, int buses, performance::PerformanceTimer* timer) : InternalPlugin(host_control),\n                                                                                          _input_buffer{buses * 2},\n                                                                                          _output_buffer{buses * 2},\n                                                                                          _buses{buses},\n                                                                                          _type{TrackType::REGULAR},\n                                                                                          _timer{timer}\n{\n    int channels = buses * 2;\n    _max_input_channels = channels;\n    _max_output_channels = channels;\n    _current_input_channels = channels;\n    _current_output_channels = channels;\n    _common_init(PanMode::PAN_AND_GAIN_PER_BUS);\n}\n\nProcessorReturnCode Track::init(float sample_rate)\n{\n    this->configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid Track::configure(float sample_rate)\n{\n    for (auto& i : _smoothers)\n    {\n        i[LEFT_CHANNEL_INDEX].set_lag_time(GAIN_SMOOTHING_TIME, sample_rate / AUDIO_CHUNK_SIZE);\n        i[RIGHT_CHANNEL_INDEX].set_lag_time(GAIN_SMOOTHING_TIME, sample_rate / AUDIO_CHUNK_SIZE);\n    }\n}\n\nbool Track::add(Processor* processor, std::optional<ObjectId> before_position)\n{\n    if (_processors.size() >= TRACK_MAX_PROCESSORS || processor == this)\n    {\n        // If a track adds itself to its process chain, endless loops can arise\n        // In addition, _processors must not allocate if running in the rt-thread\n        return false;\n    }\n    assert(processor->active_rt_processing() == false);\n\n    bool added = false;\n    if (before_position.has_value())\n    {\n        for (auto i = _processors.cbegin(); i != _processors.cend(); ++i)\n        {\n            if ((*i)->id() == *before_position) // * accesses value without throwing\n            {\n                _processors.insert(i, processor);\n                added = true;\n                break;\n            }\n        }\n    }\n    else\n    {\n        _processors.push_back(processor);\n        added = true;\n    }\n\n    if (added)\n    {\n        processor->set_event_output(this);\n        processor->set_active_rt_processing(true, _current_processing_thread);\n    }\n    return added;\n}\n\nbool Track::remove(ObjectId processor)\n{\n    for (auto i = _processors.begin(); i != _processors.end(); ++i)\n    {\n        if ((*i)->id() == processor)\n        {\n            (*i)->set_event_output(nullptr);\n            (*i)->set_active_rt_processing(false);\n            _processors.erase(i);\n            return true;\n        }\n    }\n    return false;\n}\n\nvoid Track::render()\n{\n    process_audio(_input_buffer, _output_buffer);\n    _input_buffer.clear();\n}\n\nvoid Track::process_audio(const ChunkSampleBuffer& in, ChunkSampleBuffer& out)\n{\n    /* For Tracks, process function is called from render() and the input audio data\n     * should be copied to _input_buffer prior to this call. */\n\n    auto track_timestamp = _timer->start_timer();\n\n    /* Process all the plugins in the chain, to guarantee that memory declared const is never\n     * written to, the const cast below is only done if in already points to _input_buffer\n     * (which is the case if process_audio() is called from render()), or if there is max 1\n     * plugin in the chain, in which case there will be no ping-pong copying between buffers. */\n    if (in.channel(0) == _input_buffer.channel(0) || _processors.size() <= 1)\n    {\n        _process_plugins(const_cast<ChunkSampleBuffer&>(in), out);\n    }\n    else\n    {\n        _input_buffer.replace(in);\n        _process_plugins(_input_buffer, out);\n    }\n\n    /* If there are keyboard events not consumed, pass them on upwards so the engine can process them */\n    _process_output_events();\n\n    bool muted = _mute_parameter->processed_value();\n\n    switch (_pan_mode)\n    {\n        case PanMode::GAIN_ONLY:\n            _apply_gain(out, muted);\n            break;\n\n        case PanMode::PAN_AND_GAIN:\n            _apply_pan_and_gain(out, muted);\n            break;\n\n        case PanMode::PAN_AND_GAIN_PER_BUS:\n            _apply_pan_and_gain_per_bus(out, muted);\n            break;\n    }\n\n    _timer->stop_timer_rt_safe(track_timestamp, this->id());\n}\n\nvoid Track::process_event(const RtEvent& event)\n{\n    if (is_keyboard_event(event))\n    {\n        /* Keyboard events are cached, so they can be passed on\n         * to the next processor in the track */\n        _kb_event_buffer.push(event);\n    }\n    else\n    {\n        InternalPlugin::process_event(event);\n    }\n}\n\nvoid Track::set_bypassed(bool bypassed)\n{\n    for (auto& processor : _processors)\n    {\n        processor->set_bypassed(bypassed);\n    }\n\n    Processor::set_bypassed(bypassed);\n}\n\nvoid Track::send_event(const RtEvent& event)\n{\n    if (is_keyboard_event(event))\n    {\n        _kb_event_buffer.push(event);\n    }\n    else\n    {\n        output_event(event);\n    }\n}\n\nvoid Track::_common_init(PanMode mode)\n{\n    _processors.reserve(TRACK_MAX_PROCESSORS);\n    _pan_mode = mode;\n\n    _gain_parameters.at(0) = register_float_parameter(\"gain\", \"Gain\", \"dB\",\n                                                      0.0f, -120.0f, 24.0f,\n                                                      Direction::AUTOMATABLE,\n                                                      new dBToLinPreProcessor(-120.0f, 24.0f));\n    _smoothers.emplace_back();\n\n    if (mode == PanMode::PAN_AND_GAIN || mode == PanMode::PAN_AND_GAIN_PER_BUS)\n    {\n        _pan_parameters.at(0) = register_float_parameter(\"pan\", \"Pan\", \"\",\n                                                         0.0f, -1.0f, 1.0f,\n                                                         Direction::AUTOMATABLE,\n                                                         nullptr);\n    }\n    _mute_parameter = register_bool_parameter(\"mute\", \"Mute\", \"\", false, Direction::AUTOMATABLE);\n\n    if (mode == PanMode::PAN_AND_GAIN_PER_BUS)\n    {\n        for (int bus = 1; bus < _buses; ++bus)\n        {\n            _gain_parameters.at(bus) = register_float_parameter(\"gain_sub_\" + std::to_string(bus), \"Gain\", \"dB\",\n                                                                0.0f, -120.0f, 24.0f,\n                                                                Direction::AUTOMATABLE,\n                                                                new dBToLinPreProcessor(-120.0f, 24.0f));\n\n            _pan_parameters.at(bus) = register_float_parameter(\"pan_sub_\" + std::to_string(bus), \"Pan\", \"\",\n                                                               0.0f, -1.0f, 1.0f,\n                                                               Direction::AUTOMATABLE,\n                                                               new FloatParameterPreProcessor(-1.0f, 1.0f));\n            _smoothers.emplace_back();\n        }\n    }\n\n    for (auto& i : _smoothers)\n    {\n        i[LEFT_CHANNEL_INDEX].set_direct(DEFAULT_TRACK_GAIN);\n        i[RIGHT_CHANNEL_INDEX].set_direct(DEFAULT_TRACK_GAIN);\n    }\n}\n\nvoid Track::_process_plugins(ChunkSampleBuffer& in, ChunkSampleBuffer& out)\n{\n    /* Alias the buffers, so we can swap them cheaply, without copying the underlying data */\n\n    ChunkSampleBuffer aliased_in = ChunkSampleBuffer::create_non_owning_buffer(in);\n    ChunkSampleBuffer aliased_out = ChunkSampleBuffer::create_non_owning_buffer(out);\n\n    for (auto &processor : _processors)\n    {\n        auto processor_timestamp = _timer->start_timer();\n        /* Note that processors can put events back into this queue, hence we're not draining the queue\n         * but checking the size first to avoid an infinite loop */\n        for (int kb_events = _kb_event_buffer.size(); kb_events > 0; --kb_events)\n        {\n            processor->process_event(_kb_event_buffer.pop());\n        }\n\n        ChunkSampleBuffer proc_in = ChunkSampleBuffer::create_non_owning_buffer(aliased_in, 0, processor->input_channels());\n        ChunkSampleBuffer proc_out = ChunkSampleBuffer::create_non_owning_buffer(aliased_out, 0, processor->output_channels());\n        processor->process_audio(proc_in, proc_out);\n\n        int unused_channels = aliased_out.channel_count() - processor->output_channels();\n        if (unused_channels > 0)\n        {\n            // If processor has fewer channels than the track, zero the rest to avoid passing garbage to the next processor\n            auto unused = ChunkSampleBuffer::create_non_owning_buffer(aliased_out, aliased_out.channel_count() - unused_channels, unused_channels);\n            unused.clear();\n        }\n\n        swap(aliased_in, aliased_out);\n        _timer->stop_timer_rt_safe(processor_timestamp, static_cast<int>(processor->id()));\n    }\n\n    int output_channels = _processors.empty() ? _current_output_channels : _processors.back()->output_channels();\n\n    if (output_channels > 0)\n    {\n        /* aliased_out contains the output of the last processor. If the number of processors\n         * is even, then aliased_out already points to out, otherwise we need to copy to it */\n        if (aliased_in.channel(0) == in.channel(0))\n        {\n            out.replace(aliased_in);\n        }\n    }\n    else\n    {\n        out.clear();\n    }\n}\n\nvoid Track::_process_output_events()\n{\n    while (!_kb_event_buffer.empty())\n    {\n        const RtEvent& event = _kb_event_buffer.pop();\n        switch (event.type())\n        {\n            case RtEventType::NOTE_ON:\n                output_event(RtEvent::make_note_on_event(id(), event.sample_offset(),\n                                                         event.keyboard_event()->channel(),\n                                                         event.keyboard_event()->note(),\n                                                         event.keyboard_event()->velocity()));\n                break;\n            case RtEventType::NOTE_OFF:\n                output_event(RtEvent::make_note_off_event(id(), event.sample_offset(),\n                                                          event.keyboard_event()->channel(),\n                                                          event.keyboard_event()->note(),\n                                                          event.keyboard_event()->velocity()));\n                break;\n            case RtEventType::NOTE_AFTERTOUCH:\n                output_event(RtEvent::make_note_aftertouch_event(id(), event.sample_offset(),\n                                                                 event.keyboard_event()->channel(),\n                                                                 event.keyboard_event()->note(),\n                                                                 event.keyboard_event()->velocity()));\n                break;\n            case RtEventType::AFTERTOUCH:\n                output_event(RtEvent::make_aftertouch_event(id(), event.sample_offset(),\n                                                            event.keyboard_common_event()->channel(),\n                                                            event.keyboard_common_event()->value()));\n                break;\n            case RtEventType::PITCH_BEND:\n                output_event(RtEvent::make_pitch_bend_event(id(), event.sample_offset(),\n                                                            event.keyboard_common_event()->channel(),\n                                                            event.keyboard_common_event()->value()));\n                break;\n            case RtEventType::MODULATION:\n                output_event(RtEvent::make_kb_modulation_event(id(), event.sample_offset(),\n                                                               event.keyboard_common_event()->channel(),\n                                                               event.keyboard_common_event()->value()));\n                break;\n            case RtEventType::WRAPPED_MIDI_EVENT:\n                output_event(RtEvent::make_wrapped_midi_event(id(), event.sample_offset(),\n                                                              event.wrapped_midi_event()->midi_data()));\n                break;\n\n            default:\n                output_event(event);\n        }\n    }\n    _kb_event_buffer.clear(); // Reset the read & write index to reuse the same memory area every time.\n}\n\nvoid Track::_apply_pan_and_gain(ChunkSampleBuffer& buffer, bool muted)\n{\n    assert(buffer.channel_count() <= 2);\n\n    float gain = muted ? 0.0f : _gain_parameters.front()->processed_value();\n    float pan = _pan_parameters.front()->processed_value();\n    auto[left_gain, right_gain] = calc_l_r_gain(gain, pan);\n\n    auto& left_smoother = _smoothers.front()[LEFT_CHANNEL_INDEX];\n    auto& right_smoother = _smoothers.front()[RIGHT_CHANNEL_INDEX];\n    left_smoother.set(left_gain);\n    right_smoother.set(right_gain);\n\n    ChunkSampleBuffer left = ChunkSampleBuffer::create_non_owning_buffer(buffer, LEFT_CHANNEL_INDEX, 1);\n    ChunkSampleBuffer right = ChunkSampleBuffer::create_non_owning_buffer(buffer, RIGHT_CHANNEL_INDEX, 1);\n\n    if (_current_input_channels == 1)\n    {\n        right.replace(left);\n    }\n\n    if (left_smoother.stationary() && left_smoother.stationary())\n    {\n        left.apply_gain(left_gain);\n        right.apply_gain(right_gain);\n    }\n    else // Value needs smoothing\n    {\n        left.ramp(left_smoother.value(), left_smoother.next_value());\n        right.ramp(right_smoother.value(), right_smoother.next_value());\n    }\n}\n\nvoid Track::_apply_pan_and_gain_per_bus(ChunkSampleBuffer& buffer, bool muted)\n{\n    for (int bus = 0; bus < _buses; ++bus)\n    {\n        auto bus_buffer = ChunkSampleBuffer::create_non_owning_buffer(buffer, bus * 2, 2);\n        float gain = muted ? 0.0f : _gain_parameters[bus]->processed_value();\n        float pan = _pan_parameters[bus]->processed_value();\n        auto[left_gain, right_gain] = calc_l_r_gain(gain, pan);\n        auto& left_smoother = _smoothers[bus][LEFT_CHANNEL_INDEX];\n        auto& right_smoother = _smoothers[bus][RIGHT_CHANNEL_INDEX];\n\n        left_smoother.set(left_gain);\n        right_smoother.set(right_gain);\n\n        ChunkSampleBuffer left = ChunkSampleBuffer::create_non_owning_buffer(bus_buffer, LEFT_CHANNEL_INDEX, 1);\n        ChunkSampleBuffer right = ChunkSampleBuffer::create_non_owning_buffer(bus_buffer, RIGHT_CHANNEL_INDEX, 1);\n\n        if (left_smoother.stationary() && left_smoother.stationary())\n        {\n            left.apply_gain(left_gain);\n            right.apply_gain(right_gain);\n        }\n        else // Value needs smoothing\n        {\n            left.ramp(left_smoother.value(), left_smoother.next_value());\n            right.ramp(right_smoother.value(), right_smoother.next_value());\n        }\n    }\n}\n\nvoid Track::_apply_gain(ChunkSampleBuffer& buffer, bool muted)\n{\n    float gain = muted ? 0.0f : _gain_parameters.front()->processed_value();\n\n    auto& gain_smoother = _smoothers.front()[LEFT_CHANNEL_INDEX];\n    gain_smoother.set(gain);\n\n    if (gain_smoother.stationary())\n    {\n        buffer.apply_gain(gain);\n    }\n    else // Value needs smoothing\n    {\n        buffer.ramp(gain_smoother.value(), gain_smoother.next_value());\n    }\n}\n\n} // end namespace sushi::internal::engine\n"
  },
  {
    "path": "src/engine/track.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Class to represent a mixer track with a chain of processors\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_TRACK_H\n#define SUSHI_TRACK_H\n\n#include <string>\n#include <memory>\n#include <array>\n#include <vector>\n\n#include \"sushi/constants.h\"\n#include \"sushi/sample_buffer.h\"\n\n#include \"library/internal_plugin.h\"\n#include \"library/performance_timer.h\"\n#include \"library/rt_event_fifo.h\"\n\n#include \"dsp_library/value_smoother.h\"\n\nnamespace sushi::internal::engine {\n\n/* No real technical limit, just something arbitrarily high enough */\nconstexpr int MAX_TRACK_BUSES = MAX_TRACK_CHANNELS / 2;\nconstexpr int KEYBOARD_EVENT_QUEUE_SIZE = 256;\n\nenum class TrackType\n{\n    REGULAR,\n    PRE,\n    POST\n};\n\nclass TrackAccessor;\n\nclass Track : public InternalPlugin, public RtEventPipe\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(Track);\n\n    /**\n     * @brief Create a track\n     * @param host_control Host callback object\n     * @param channels The number of channels in the track\n     * @param timer A timer object\n     * @param pan_controls If true, create a pan control parameter on the track\n     */\n    Track(HostControl host_control, int channels, performance::PerformanceTimer* timer, bool pan_controls, TrackType type = TrackType::REGULAR);\n\n    /**\n     * @brief Create a track with a given number of stereo input and output buses\n     *        buses are an abstraction for buses*2 channels internally.\n     * @param host_control Host callback object\n     * @param timer A timer object\n     * @param buses The number of stereo audio buses\n     */\n    Track(HostControl host_control, int buses, performance::PerformanceTimer* timer);\n\n    ~Track() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    /**\n     * @brief Add a processor to the track's processing chain at the position before\n     *        The processor with id before_position.\n     *        Should be called from the audio thread or when the track is not processing.\n     * @param processor A pointer to the plugin instance to add.\n     * @param before_position The ObjectId of the succeeding plugin, if not set, the\n     *        processor will be added to the back of the track\n     * @return true if the insertion was successful, false otherwise\n     */\n    bool add(Processor* processor, std::optional<ObjectId> before_position = std::nullopt);\n\n    /**\n     * @brief Remove a plugin from the track.\n     * @param processor The ObjectId of the processor to remove\n     * @return true if the processor was found and successfully removed, false otherwise\n     */\n    bool remove(ObjectId processor);\n\n    /**\n     * @brief Return a SampleBuffer to an input bus\n     * @param bus The index of the bus, must not be greater than the number of buses configured\n     * @return A non-owning SampleBuffer pointing to the given bus\n     */\n    ChunkSampleBuffer input_bus(int bus)\n    {\n        assert(bus < _buses);\n        return ChunkSampleBuffer::create_non_owning_buffer(_input_buffer, bus * 2, 2);\n    }\n\n    /**\n    * @brief Return a SampleBuffer to an output bus\n    * @param bus The index of the bus, must not be greater than the number of buses configured\n    * @return A non-owning SampleBuffer pointing to the given bus\n    */\n    ChunkSampleBuffer output_bus(int bus)\n    {\n        assert(bus < _buses);\n        return ChunkSampleBuffer::create_non_owning_buffer(_output_buffer, bus * 2, 2);\n    }\n\n    /**\n     * @brief Return a SampleBuffer to an input channel\n     * @param bus The index of the channel, must not be greater than the number of channels configured\n     * @return A non-owning SampleBuffer pointing to the given bus\n     */\n    ChunkSampleBuffer input_channel(int index)\n    {\n        assert(index < _max_input_channels);\n        return ChunkSampleBuffer::create_non_owning_buffer(_input_buffer, index, 1);\n    }\n\n    /**\n    * @brief Return a SampleBuffer to an output bus\n    * @param bus The index of the channel, must not be greater than the number of channels configured\n    * @return A non-owning SampleBuffer pointing to the given bus\n    */\n    ChunkSampleBuffer output_channel(int index)\n    {\n        assert(index < _max_output_channels);\n        return ChunkSampleBuffer::create_non_owning_buffer(_output_buffer, index, 1);\n    }\n\n    /**\n     * @brief Return the number of stereo buses of the track.\n     * @return The number of stereo buses on the track.\n     */\n    [[nodiscard]] int buses() const\n    {\n        return _buses;\n    }\n\n    /**\n     * @brief Return the current audio thread processing the track.\n     * @return The audio thread processing the track.\n     */\n    [[nodiscard]] int thread() const\n    {\n        return _current_processing_thread;\n    }\n\n    /**\n     * @brief Render all processors of the track. Should be called after process_event() and\n     *        after input buffers have been filled\n     */\n    void render();\n\n    /**\n     * @brief Static render function for passing to a thread manager\n     * @param arg Void* pointing to an instance of a Track.\n     */\n    static void ext_render_function(void* arg)\n    {\n        reinterpret_cast<Track*>(arg)->render();\n    }\n\n    [[nodiscard]] TrackType type() const\n    {\n        return _type;\n    }\n\n    /* Inherited from Processor */\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer& in, ChunkSampleBuffer& out) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    /* Inherited from RtEventPipe */\n    void send_event(const RtEvent& event) override;\n\nprivate:\n    friend TrackAccessor;\n\n    enum class PanMode\n    {\n        GAIN_ONLY,\n        PAN_AND_GAIN,\n        PAN_AND_GAIN_PER_BUS\n    };\n\n    void _common_init(PanMode mode);\n    void _process_plugins(ChunkSampleBuffer& in, ChunkSampleBuffer& out);\n    void _process_output_events();\n    void _apply_pan_and_gain(ChunkSampleBuffer& buffer, bool muted);\n    void _apply_pan_and_gain_per_bus(ChunkSampleBuffer& buffer, bool muted);\n    void _apply_gain(ChunkSampleBuffer& buffer, bool muted);\n\n    std::vector<Processor*> _processors;\n    ChunkSampleBuffer _input_buffer;\n    ChunkSampleBuffer _output_buffer;\n\n    int _buses;\n    PanMode _pan_mode;\n    TrackType _type;\n\n    BoolParameterValue*                               _mute_parameter;\n    std::array<FloatParameterValue*, MAX_TRACK_BUSES> _gain_parameters;\n    std::array<FloatParameterValue*, MAX_TRACK_BUSES> _pan_parameters;\n    std::vector<std::array<ValueSmootherFilter<float>, 2>> _smoothers;\n\n    performance::PerformanceTimer* _timer;\n\n    RtEventFifo<KEYBOARD_EVENT_QUEUE_SIZE> _kb_event_buffer;\n};\n\n} // end namespace sushi::internal::engine\n\n#endif // SUSHI_TRACK_H\n"
  },
  {
    "path": "src/engine/transport.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Main entry point to Sushi\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n#include <cmath>\n#include <algorithm>\n\n#ifdef SUSHI_BUILD_WITH_ABLETON_LINK\n#include \"ableton/Link.hpp\"\n#ifdef _MSC_VER\n#undef DELETE // Because Link pulls in Windows headers\n#endif\n#else //SUSHI_BUILD_WITH_ABLETON_LINK\n#include \"link_dummy.h\"\n#endif // SUSHI_BUILD_WITH_ABLETON_LINK\n\n#include \"elklog/static_logger.h\"\n\n#include \"sushi/constants.h\"\n#include \"library/rt_event.h\"\n#include \"transport.h\"\n\nnamespace sushi::internal::engine {\n\nconstexpr float MIN_TEMPO = 20.0;\nconstexpr float MAX_TEMPO = 1000.0;\nconstexpr float PPQN_FLOAT = SUSHI_PPQN_TICK;\n\n\n#if SUSHI_BUILD_WITH_ABLETON_LINK\n\n/**\n * @brief Custom realtime clock for Link\n *        It is necessary to compile Link with another Clock implementation than the standard one\n *        as calling clock_get_time() is not safe to do from a Xenomai thread. Instead we supply\n *        our own clock implementation based on twine, which provides a threadsafe implementation\n *        for calling from both xenomai and posix contexts.\n */\nclass RtSafeClock\n{\npublic:\n    [[nodiscard]] std::chrono::microseconds micros() const\n    {\n        auto time = twine::current_rt_time();\n        return std::chrono::microseconds(std::chrono::duration_cast<std::chrono::microseconds>(time));\n    }\n};\n\n/**\n * @brief Wrapping Link with a custom clock\n */\nclass SushiLink : public ::ableton::BasicLink<RtSafeClock>\n{\npublic:\n  using Clock = RtSafeClock;\n\n  explicit SushiLink(double bpm) : ::ableton::BasicLink<Clock>(bpm) { }\n};\n\n#endif\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"transport\");\n\nvoid peer_callback([[maybe_unused]] size_t peers)\n{\n    ELKLOG_LOG_INFO(\"Ableton link reports {} peers connected \", peers);\n}\n\nvoid tempo_callback([[maybe_unused]] double tempo)\n{\n    ELKLOG_LOG_DEBUG(\"Ableton link reports tempo is now {} bpm \", tempo);\n}\n\nvoid start_stop_callback([[maybe_unused]] bool playing)\n{\n    ELKLOG_LOG_INFO(\"Ableton link reports {}\", playing? \"now playing\" : \"now stopped\");\n}\n\ninline bool valid_time_signature(const TimeSignature& sig)\n{\n    return sig.numerator > 0 && sig.denominator > 0;\n}\n\nTransport::Transport(float sample_rate,\n                     RtEventPipe* rt_event_pipe) : _samplerate(sample_rate),\n                                                   _rt_event_dispatcher(rt_event_pipe),\n                                                   _link_controller(std::make_unique<SushiLink>(DEFAULT_TEMPO))\n{\n    _link_controller->setNumPeersCallback(peer_callback);\n    _link_controller->setTempoCallback(tempo_callback);\n    _link_controller->setStartStopCallback(start_stop_callback);\n    _link_controller->enableStartStopSync(true);\n}\n\nTransport::~Transport() = default;\n\nvoid Transport::set_time(Time timestamp, int64_t samples)\n{\n    _time = timestamp + _latency;\n    int64_t prev_samples = _sample_count;\n    _sample_count = samples;\n    _state_change = PlayStateChange::UNCHANGED;\n\n    int64_t sample_delta = samples - prev_samples;\n    ELKLOG_LOG_WARNING_IF(sample_delta != AUDIO_CHUNK_SIZE && sample_delta != 0, \"Unexpected sample delta: {}. Possibly due to over or under runs\", sample_delta);\n\n    _update_internals();\n\n    switch (_syncmode)\n    {\n        case SyncMode::MIDI:       // Midi and Gate not implemented, so treat like internal\n        case SyncMode::GATE_INPUT:\n        case SyncMode::INTERNAL:\n        {\n            _update_internal_sync(sample_delta);\n            break;\n        }\n\n        case SyncMode::ABLETON_LINK:\n        {\n            _update_link_sync(_time);\n            break;\n        }\n    }\n\n    if (_playmode != PlayingMode::STOPPED)\n    {\n        _output_ppqn_ticks();\n    }\n}\n\nvoid Transport::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n        case RtEventType::TEMPO:\n            _set_tempo = std::clamp(event.tempo_event()->tempo(), MIN_TEMPO, MAX_TEMPO);\n            break;\n\n        case RtEventType::TIME_SIGNATURE:\n        {\n            auto time_signature = event.time_signature_event()->time_signature();\n            if (time_signature != _time_signature && valid_time_signature(time_signature))\n            {\n                _time_signature = time_signature;\n                _rt_event_dispatcher->send_event(RtEvent::make_time_signature_event(0, _time_signature));\n            }\n            break;\n        }\n\n        case RtEventType::PLAYING_MODE:\n            _set_playmode = event.playing_mode_event()->mode();\n            break;\n\n        case RtEventType::SYNC_MODE:\n        {\n            auto mode = event.sync_mode_event()->mode();\n#ifndef SUSHI_BUILD_WITH_ABLETON_LINK\n            if (mode == SyncMode::ABLETON_LINK)\n            {\n                mode = SyncMode::INTERNAL;\n            }\n#endif\n            if (mode != _syncmode)\n            {\n                _syncmode = mode;\n                _rt_event_dispatcher->send_event(RtEvent::make_sync_mode_event(0, _syncmode));\n            }\n            break;\n        }\n\n        default:\n            break;\n    }\n}\n\nvoid Transport::set_time_signature(TimeSignature signature, bool update_via_event)\n{\n    if (valid_time_signature(signature))\n    {\n        if (update_via_event == false)\n        {\n            _time_signature = signature;\n        }\n        if (_link_controller->isEnabled())\n        {\n            _set_link_quantum(signature);\n        }\n    }\n}\n\nvoid Transport::set_tempo(float tempo, bool update_via_event)\n{\n    tempo = std::clamp(tempo, MIN_TEMPO, MAX_TEMPO);\n    if (update_via_event == false)\n    {\n        _set_tempo = tempo;\n        _tempo = tempo;\n    }\n    _set_link_tempo(tempo);\n}\n\nvoid Transport::set_playing_mode(PlayingMode mode, bool update_via_event)\n{\n    bool playing = mode != PlayingMode::STOPPED;\n    bool update = this->playing() != playing;\n    if (update)\n    {\n        if (_link_controller->isEnabled())\n        {\n            _set_link_playing(playing);\n        }\n    }\n\n    if (update_via_event == false)\n    {\n        _set_playmode = mode;\n    }\n}\n\nvoid Transport::set_sync_mode(SyncMode mode, bool update_via_event)\n{\n#ifndef SUSHI_BUILD_WITH_ABLETON_LINK\n    if (mode == SyncMode::ABLETON_LINK)\n    {\n        ELKLOG_LOG_INFO(\"Ableton Link sync mode requested, but sushi was built without Link support\");\n        return;\n    }\n#endif\n    switch (mode)\n    {\n        case SyncMode::INTERNAL:\n        case SyncMode::MIDI:\n        case SyncMode::GATE_INPUT:\n            _link_controller->enable(false);\n            break;\n        case SyncMode::ABLETON_LINK:\n            _link_controller->enable(true);\n            _set_link_playing(_set_playmode != PlayingMode::STOPPED);\n            break;\n    }\n    if (update_via_event == false)\n    {\n        _syncmode = mode;\n    }\n}\n\ndouble Transport::current_bar_beats(int samples) const\n{\n    if (_playmode != PlayingMode::STOPPED)\n    {\n        double offset = _beats_per_chunk * static_cast<double>(samples) / AUDIO_CHUNK_SIZE;\n        return std::fmod(_current_bar_beat_count + offset, _beats_per_bar);\n    }\n    return _current_bar_beat_count;\n}\n\ndouble Transport::current_beats(int samples) const\n{\n    if (_playmode != PlayingMode::STOPPED)\n    {\n        return _beat_count + _beats_per_chunk * static_cast<double>(samples) / AUDIO_CHUNK_SIZE;\n    }\n    return _beat_count;\n}\n\nvoid Transport::_update_internals()\n{\n    assert(_samplerate > 0.0f);\n    /* Time signatures are seen in relation to 4/4 and remapped to quarter notes\n     * the same way most DAWs do it. This makes 3/4 and 6/8 behave identically, and\n     * they will play beatsynched with 4/4, i.e. not on triplets. */\n    _beats_per_bar = 4.0f * static_cast<float>(_time_signature.numerator) /\n                            static_cast<float>(_time_signature.denominator);\n}\n\nvoid Transport::_update_internal_sync(int64_t samples)\n{\n    // We cannot assume chunk size is an absolute multiple of samples for all buffer sizes.\n    auto chunks_passed = static_cast<double>(samples) / AUDIO_CHUNK_SIZE;\n\n    if (_playmode != _set_playmode)\n    {\n        _state_change = _set_playmode == PlayingMode::STOPPED? PlayStateChange::STOPPING : PlayStateChange::STARTING;\n        _playmode = _set_playmode;\n        // Notify of new playing mode\n        _rt_event_dispatcher->send_event(RtEvent::make_playing_mode_event(0, _set_playmode));\n    }\n\n    double beats_per_chunk = _set_tempo / 60.0 * static_cast<double>(AUDIO_CHUNK_SIZE) / _samplerate;\n\n    _beats_per_chunk = beats_per_chunk;\n\n    if (_state_change == PlayStateChange::STARTING) // Reset bar beat count when starting\n    {\n        _current_bar_beat_count = 0.0;\n        _beat_count = 0.0;\n        _bar_start_beat_count = 0.0;\n    }\n    else if (_playmode != PlayingMode::STOPPED)\n    {\n        if (_position_source == PositionSource::CALCULATED)\n        {\n            _current_bar_beat_count += chunks_passed * beats_per_chunk;\n            if (_current_bar_beat_count > _beats_per_bar)\n            {\n                _current_bar_beat_count = std::fmod(_current_bar_beat_count, _beats_per_bar);\n                _bar_start_beat_count += _beats_per_bar;\n            }\n            _beat_count += chunks_passed * beats_per_chunk;\n        }\n        else\n        {\n            _bar_start_beat_count = _beat_count - _current_bar_beat_count;\n        }\n    }\n\n    if (_tempo != _set_tempo)\n    {\n        // Notify of tempo change\n        _rt_event_dispatcher->send_event(RtEvent::make_tempo_event(0, _set_tempo));\n        _tempo = _set_tempo;\n    }\n}\n\nvoid Transport::_update_link_sync(Time timestamp)\n{\n    auto session = _link_controller->captureAudioSessionState();\n    auto tempo = static_cast<float>(session.tempo());\n    if (tempo != _set_tempo)\n    {\n        _set_tempo = tempo;\n        // Notify of new tempo\n        _rt_event_dispatcher->send_event(RtEvent::make_tempo_event(0, tempo));\n    }\n    _tempo = tempo;\n\n    assert(_position_source == PositionSource::CALCULATED);\n\n    if (session.isPlaying() != this->playing())\n    {\n        auto new_playing_mode = session.isPlaying() ? PlayingMode::PLAYING: PlayingMode::STOPPED;\n        _state_change = new_playing_mode == PlayingMode::STOPPED? PlayStateChange::STOPPING : PlayStateChange::STARTING;\n        _playmode = new_playing_mode;\n        _set_playmode = new_playing_mode;\n        // Notify of new playing mode\n        _rt_event_dispatcher->send_event(RtEvent::make_playing_mode_event(0, _set_playmode));\n    }\n\n    _beats_per_chunk =  _tempo / 60.0 * static_cast<double>(AUDIO_CHUNK_SIZE) / _samplerate;\n\n    if (session.isPlaying())\n    {\n        _beat_count = session.beatAtTime(timestamp, _beats_per_bar);\n        _current_bar_beat_count = session.phaseAtTime(timestamp, _beats_per_bar);\n        _bar_start_beat_count = _beat_count - _current_bar_beat_count;\n    }\n\n    /* Due to the nature of the Xenomai RT architecture we cannot commit changes to\n     * the session here as that would cause a mode switch. Instead, all changes need\n     * to be made from the non rt thread */\n}\n\nvoid Transport::_output_ppqn_ticks()\n{\n    double beat = current_beats();\n    if (current_state_change() == PlayStateChange::STARTING)\n    {\n        _last_tick_sent = beat;\n    }\n\n    double last_beat_this_chunk = current_beats(AUDIO_CHUNK_SIZE);\n    double beat_period = last_beat_this_chunk - beat;\n    auto tick_this_chunk = std::min(PPQN_FLOAT * (last_beat_this_chunk - _last_tick_sent), 2.0);\n\n    while (tick_this_chunk >= 1.0)\n    {\n        double next_note_beat = _last_tick_sent + 1.0 / PPQN_FLOAT;\n        auto fraction = next_note_beat - beat - std::floor(next_note_beat - beat);\n        _last_tick_sent = next_note_beat;\n        int offset = 0;\n        if (fraction > 0)\n        {\n            /* If fraction is not positive, then there was a missed beat in an underrun */\n            offset = std::min(static_cast<int>(std::round(AUDIO_CHUNK_SIZE * fraction / beat_period)),\n                              AUDIO_CHUNK_SIZE - 1);\n        }\n\n        RtEvent tick = RtEvent::make_timing_tick_event(offset, 0);\n        _rt_event_dispatcher->send_event(tick);\n        tick_this_chunk = PPQN_FLOAT * (last_beat_this_chunk - _last_tick_sent);\n    }\n}\n\n#ifdef SUSHI_BUILD_WITH_ABLETON_LINK\nvoid Transport::_set_link_playing(bool playing)\n{\n    auto session = _link_controller->captureAppSessionState();\n    session.setIsPlaying(playing, _time);\n    if (playing)\n    {\n        session.requestBeatAtTime(_beat_count, _time, _beats_per_bar);\n        _link_controller->commitAppSessionState(session);\n    }\n}\n\nvoid Transport::_set_link_tempo(float tempo)\n{\n    auto state = _link_controller->captureAppSessionState();\n    state.setTempo(tempo, this->current_process_time());\n    _link_controller->commitAudioSessionState(state);\n}\n\nvoid Transport::_set_link_quantum(TimeSignature signature)\n{\n    auto session = _link_controller->captureAppSessionState();\n    if (session.isPlaying())\n    {\n        // TODO - Consider what to do for non-integer quanta. Multiply with gcd until we get an integer ratio?\n        int quantum = std::max(1, (4 * signature.denominator) / signature.numerator);\n        session.requestBeatAtTime(_beat_count, _time, quantum);\n        _link_controller->commitAppSessionState(session);\n    }\n}\n\n#else\nvoid Transport::_set_link_playing(bool /*playing*/) {}\nvoid Transport::_set_link_tempo(float /*tempo*/) {}\nvoid Transport::_set_link_quantum(TimeSignature /*signature*/) {}\n#endif\n\n} // end namespace sushi::internal::engine"
  },
  {
    "path": "src/engine/transport.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Handles time, tempo and start/stop inside the engine\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_TRANSPORT_H\n#define SUSHI_TRANSPORT_H\n\n#include <memory>\n#include <atomic>\n#include <cassert>\n\n#include \"sushi/constants.h\"\n#include \"sushi/types.h\"\n#include \"sushi/sushi_time.h\"\n\n#include \"library/event_interface.h\"\n#include \"library/rt_event.h\"\n#include \"library/rt_event_pipe.h\"\n#include \"engine/base_event_dispatcher.h\"\n\n/* Forward declare Link, then we can include link.hpp from transport.cpp and\n * nowhere else, it pulls in a lot of dependencies that slow down compilation\n * significantly otherwise. */\nnamespace ableton {class Link;}\n\nnamespace sushi::internal {\n\nenum class PlayStateChange\n{\n    UNCHANGED,\n    STARTING,\n    STOPPING,\n};\n\nenum class PositionSource\n{\n    EXTERNAL,\n    CALCULATED\n};\n\nnamespace engine {\n\nclass SushiLink;\n\nconstexpr float DEFAULT_TEMPO = 120;\n\nclass Transport\n{\npublic:\n    explicit Transport(float sample_rate, RtEventPipe* rt_event_pipe);\n    ~Transport();\n\n    /**\n     * @brief Set the current time. Called from the audio thread\n     * @param timestamp The current timestamp\n     * @param samples   The number of samples passed\n     */\n    void set_time(Time timestamp, int64_t samples);\n\n    /**\n     * @brief Set the output latency, i.e. the time it takes for the audio to travel through\n     *        the driver stack to a physical output, including any DAC latency. Should be\n     *        called by the audio frontend\n     * @param output_latency The output latency\n     */\n    void set_latency(Time output_latency)\n    {\n        _latency = output_latency;\n    }\n\n    /**\n     * @brief Process a single realtime event that is to take place during the current audio\n     * interrupt\n     * @param event Event to process.\n     */\n    void process_event(const RtEvent& event);\n\n    /**\n     * @brief Set the time signature used in the engine. Called from a non-realtime thread.\n     * @param signature The new time signature to use\n     * @param update_via_event Set to true if the engine is running in realtime and will\n     *                         pass the update as an RtEvent\n     */\n    void set_time_signature(TimeSignature signature, bool update_via_event);\n\n    /**\n     * @brief Set the tempo of the engine in beats (quarter notes) per minute. Called from\n     *        a non-realtime thread.\n     * @param tempo The new tempo in beats per minute\n     * @param update_via_event Set to true if the engine is running in realtime and will\n     *                         pass the update as an RtEvent\n     */\n    void set_tempo(float tempo, bool update_via_event);\n\n    /**\n     * @brief Return the current set playing mode\n     * @return the current set playing mode\n     */\n    [[nodiscard]] PlayingMode playing_mode() const { return _playmode; }\n\n    /**\n     * @brief Set the playing mode, i.e. playing, stopped, recording etc.. Called from\n     *        a non-realtime thread.\n     * @param mode The new playing mode.\n     * @param update_via_event Set to true if the engine is running in realtime and will\n     *                         pass the update as an RtEvent\n     */\n    void set_playing_mode(PlayingMode mode, bool update_via_event);\n\n    /**\n     * @brief Return the current current mode of synchronising tempo and beats\n     * @return the current set mode of synchronisation\n     */\n    [[nodiscard]] SyncMode sync_mode() const { return _syncmode; }\n\n    /**\n     * @brief Set the current mode of synchronising tempo and beats. Called from\n     *        a non-realtime thread.\n     * @param mode The mode of synchronisation to use\n     * @param update_via_event true if the engine passes the update as an RtEvent\n     */\n    void set_sync_mode(SyncMode mode, bool update_via_event);\n\n    /**\n     * @return Set the sample rate.\n     * @param sample_rate The current sample rate the engine is running at\n     */\n    void set_sample_rate(float sample_rate) { _samplerate = sample_rate; }\n\n    /**\n     * @brief Query the current time, Safe to call from rt and non-rt context. If called from\n     *        an rt context it will return the time at which sample 0 of the current audio\n     *        chunk being processed will appear on an output.\n     * @return The current processing time.\n     */\n    [[nodiscard]] Time current_process_time() const { return _time; }\n\n    /**\n     * @brief Query the current samplecount. Safe to call from rt and non-rt context. If called\n     *        from an rt context it will return the total number of samples passed before sample\n     *        0 of the current audio chunk being processed.\n     * @return Total samplecount\n     */\n    [[nodiscard]] int64_t current_samples() const { return _sample_count; }\n\n    /**\n     * @brief If the transport is currently playing or not.\n     * @return true if the transport is currently playing, false if stopped\n     */\n    [[nodiscard]] bool playing() const { return _playmode != PlayingMode::STOPPED; }\n\n    /**\n     * @brief Query the current time signature being used\n     * @return A TimeSignature struct describing the current time signature\n     */\n    [[nodiscard]] TimeSignature time_signature() const { return _time_signature; }\n\n    /**\n     * @brief Query the current tempo. Safe to call from rt and non-rt context but will\n     *        only return approximate value if called from a non-rt context. If sync is\n     *        not set to INTERNAL, this might be different than what was previously used\n     *        as an argument to set_tempo()\n     * @return A float representing the tempo in beats per minute\n     */\n    [[nodiscard]] float current_tempo() const { return _tempo; }\n\n    /**\n    * @brief Query the position in beats (quarter notes) in the current bar with an optional\n    *        sample offset from the start of the current processing chunk.\n    *        The number of quarter notes in a bar will depend on the time signature set.\n    *        For 4/4 time this will return a float in the range (0, 4), for 6/8 time this\n    *        will return a range (0, 2). Safe to call from rt and non-rt context but will\n    *        only return approximate values if called from a non-rt context\n    * @param samples The number of samples offset form sample 0 in the current audio chunk\n    * @return A double representing the position in the current bar.\n    */\n    [[nodiscard]] double current_bar_beats(int samples) const;\n    [[nodiscard]] double current_bar_beats() const { return _current_bar_beat_count; }\n\n    /**\n     * @brief Query the current position in beats (quarter notes( with an optional sample\n     *        offset from the start of the current processing chunk. This runs continuously\n     *        and is monotonically increasing. Safe to call from rt and non-rt context but\n     *        will only return approximate values if called from a non-rt context\n     * @param samples The number of samples offset form sample 0 in the current audio chunk\n     * @return A double representing the current position in quarter notes.\n     */\n    [[nodiscard]] double current_beats(int samples) const;\n    [[nodiscard]] double current_beats() const { return _beat_count; }\n\n    /**\n     * @return Query the position, in beats (quarter notes), of the start of the current bar.\n     *         Safe to call from rt and non-rt context but will only return approximate values\n     *         if called from a non-rt context\n     * @return A double representing the start position of the current bar in quarter notes\n     */\n    [[nodiscard]] double current_bar_start_beats() const { return _bar_start_beat_count; }\n\n    /**\n     * @brief Sets which source to use for the beat count position: the internally calculated one, or the one set\n     *        using the set_current_beats method below.\n     * @param position_source Enum, EXTERNAL / CALCULATED\n     */\n    void set_position_source(PositionSource position_source) {_position_source = position_source;}\n\n    /**\n     * @brief Returns the position source setting.\n     * @return PositionSource enum\n     */\n    [[nodiscard]] PositionSource position_source() { return _position_source; }\n\n    /**\n     * @brief Sets the beat count value. This is to be used alongside a position_source set to EXTERNAL,\n     *        or the set value will be overwritten by the internal calculation.\n     * @param beat_count double value\n     */\n    void set_current_beats(double beat_count) { _beat_count = beat_count; }\n\n    /**\n     * @brief Sets the bar beat count value. This is to be used alongside a position_source set to EXTERNAL,\n     *        or the set value will be overwritten by the internal calculation.\n     * @param bar_beat_count\n     */\n    void set_current_bar_beats(double bar_beat_count) { _current_bar_beat_count = bar_beat_count; }\n\n    /**\n     * @brief Query any playing state changes occurring during the current processing chunk.\n     *        For instance, if Transport is starting, during the first chunk, Transport\n     *        will report playing() as true and current_state_change() as STARTING.\n     *        Subsequent chunks Transport will report playing() as true and\n     *        current_state_change() as UNCHANGED.\n     * @return A PlayStateChange enum with the current state change, if any.\n     */\n    [[nodiscard]] PlayStateChange current_state_change() const { return _state_change; }\n\nprivate:\n    void _update_internals();\n    void _update_internal_sync(int64_t samples);\n    void _update_link_sync(Time timestamp);\n    void _output_ppqn_ticks();\n    void _set_link_playing(bool playing);\n    void _set_link_tempo(float tempo);\n    void _set_link_quantum(TimeSignature signature);\n\n    int64_t         _sample_count {0};\n    Time            _time {0};\n    Time            _latency {0};\n    double          _current_bar_beat_count {0.0};\n    double          _beat_count {0.0};\n    double          _bar_start_beat_count {0};\n    double          _beats_per_chunk {0};\n    double          _beats_per_bar {0};\n    float           _samplerate {0};\n\n    double          _last_tick_sent {0};\n\n    float           _tempo {DEFAULT_TEMPO};\n    float           _set_tempo {_tempo};\n    PlayingMode     _playmode {PlayingMode::STOPPED};\n    PlayingMode     _set_playmode {_playmode};\n    SyncMode        _syncmode {SyncMode::INTERNAL};\n    TimeSignature   _time_signature {4, 4};\n    PlayStateChange _state_change {PlayStateChange::STARTING};\n\n    RtEventPipe*    _rt_event_dispatcher;\n\n    std::unique_ptr<SushiLink> _link_controller;\n\n    PositionSource _position_source {PositionSource::CALCULATED};\n};\n\n} // end namespace engine\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_TRANSPORT_H\n"
  },
  {
    "path": "src/factories/base_factory.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Public Base class for all Sushi factory implementations.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"base_factory.h\"\n\n#include \"elklog/static_logger.h\"\n\n#include \"engine/audio_engine.h\"\n#include \"engine/json_configurator.h\"\n\n#include \"concrete_sushi.h\"\n#include \"sushi/utils.h\"\n\n#include \"audio_frontends/portaudio_frontend.h\"\n#include \"audio_frontends/apple_coreaudio_frontend.h\"\n\n#include \"control_frontends/oscpack_osc_messenger.h\"\n\nnamespace sushi::internal {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"base-factory\");\n\nBaseFactory::BaseFactory() = default;\n\nBaseFactory::~BaseFactory() = default;\n\nstd::unique_ptr<Sushi> BaseFactory::_make_sushi()\n{\n    if (_status == Status::OK)\n    {\n        // BaseFactory is a friend of ConcreteSushi:\n        // meaning it's impossible to instantiate Sushi without inheriting from BaseFactory.\n        auto sushi = std::unique_ptr<ConcreteSushi>(new ConcreteSushi());\n\n        sushi->_engine = std::move(_engine);\n        sushi->_midi_dispatcher = std::move(_midi_dispatcher);\n        sushi->_midi_frontend = std::move(_midi_frontend);\n        sushi->_osc_frontend = std::move(_osc_frontend);\n        sushi->_audio_frontend = std::move(_audio_frontend);\n        sushi->_frontend_config = std::move(_frontend_config);\n        sushi->_engine_controller = std::move(_engine_controller);\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n        sushi->_rpc_server = std::move(_rpc_server);\n#endif\n        return sushi;\n    }\n    else\n    {\n        return nullptr;\n    }\n}\n\nvoid BaseFactory::_instantiate_subsystems(SushiOptions& options)\n{\n\n#ifdef SUSHI_APPLE_THREADING\n    switch (options.frontend_type)\n    {\n#ifdef SUSHI_BUILD_WITH_PORTAUDIO\n        case FrontendType::PORTAUDIO:\n        {\n            options.device_name = sushi::internal::audio_frontend::get_portaudio_output_device_name(options.portaudio_output_device_id);\n            break;\n        }\n#endif\n\n#ifdef SUSHI_BUILD_WITH_APPLE_COREAUDIO\n        case FrontendType::APPLE_COREAUDIO:\n        {\n            options.device_name = sushi::internal::audio_frontend::get_coreaudio_output_device_name(options.apple_coreaudio_output_device_uid);\n            break;\n        }\n#endif\n        default:\n        {\n            options.device_name = std::nullopt;\n        }\n    }\n#endif\n\n    _engine = std::make_unique<engine::AudioEngine>(static_cast<float>(SUSHI_SAMPLE_RATE_DEFAULT),\n                                                    options.rt_cpu_cores,\n                                                    options.device_name,\n                                                    options.debug_mode_switches,\n                                                    nullptr);\n\n    if (!options.base_plugin_path.empty())\n    {\n        _engine->set_base_plugin_path(options.base_plugin_path);\n    }\n\n    _midi_dispatcher = std::make_unique<midi_dispatcher::MidiDispatcher>(_engine->event_dispatcher());\n\n    if (options.config_source == ConfigurationSource::FILE)\n    {\n        _status = _configure_from_file(options);\n    }\n    else if (options.config_source == ConfigurationSource::JSON_STRING)\n    {\n        _status = _configure_from_json(options);\n    }\n    else if (options.config_source == ConfigurationSource::NONE)\n    {\n        _status = _configure_with_defaults(options);\n    }\n\n    if (_status == Status::OK)\n    {\n        _status = _configure_timing_options(options);\n    }\n}\n\nStatus BaseFactory::_configure_from_file(SushiOptions& options)\n{\n    auto config = read_file(options.config_filename);\n\n    if (!config)\n    {\n        return Status::FAILED_INVALID_FILE_PATH;\n    }\n\n    options.json_string = config.value();\n\n    return _configure_from_json(options);\n}\n\nStatus BaseFactory::_configure_from_json(SushiOptions& options)\n{\n    auto configurator = std::make_unique<jsonconfig::JsonConfigurator>(_engine.get(),\n                                                                       _midi_dispatcher.get(),\n                                                                       _engine->processor_container(),\n                                                                       options.json_string);\n\n    auto [control_config_status, control_config] = configurator->load_control_config();\n    if (control_config_status != jsonconfig::JsonConfigReturnStatus::OK)\n    {\n        return Status::FAILED_INVALID_CONFIGURATION_FILE;\n    }\n\n    auto engine_status = _configure_engine(options, control_config, configurator.get());\n    if (engine_status != Status::OK)\n    {\n        return engine_status;\n    }\n\n    auto configuration_status = _load_json_configuration(configurator.get());\n    if (configuration_status != Status::OK)\n    {\n        return configuration_status;\n    }\n\n    auto event_status = _load_json_events(options, configurator.get());\n    if (event_status != Status::OK)\n    {\n        return event_status;\n    }\n\n    return Status::OK;\n}\n\nStatus BaseFactory::_configure_with_defaults(SushiOptions& options)\n{\n    jsonconfig::ControlConfig control_config;\n    control_config.midi_inputs = 1;\n    control_config.midi_outputs = 1;\n    control_config.cv_inputs = 0;\n    control_config.cv_outputs = 0;\n\n    return _configure_engine(options, control_config, nullptr); // nullptr for configurator\n}\n\nStatus BaseFactory::_configure_timing_options(const SushiOptions& options)\n{\n    if (options.enable_timings || !options.detailed_timing_log.empty())\n    {\n        _engine->performance_timer()->enable(true);\n        for (const auto& name : options.detailed_timing_log)\n        {\n            auto processor = _engine->processor_container()->processor(name);\n            if (!processor)\n            {\n                ELKLOG_LOG_INFO(\"Processor {} not found\", name);\n                return Status::FAILED_LOAD_TRACKS;\n            }\n            _engine->performance_timer()->enable_detailed_timings(processor->id(), true);\n            ELKLOG_LOG_INFO(\"Enabled detailed timing logging for processor {} ({})\", name, processor->id());\n        }\n    }\n    return Status::OK;\n}\n\nStatus BaseFactory::_configure_engine(SushiOptions& options,\n                                      const jsonconfig::ControlConfig& control_config,\n                                      jsonconfig::JsonConfigurator* configurator)\n{\n    auto status = _setup_audio_frontend(options, control_config);\n\n    auto audio_frontend_status = _audio_frontend->init(_frontend_config.get());\n    if (audio_frontend_status != audio_frontend::AudioFrontendStatus::OK)\n    {\n        return Status::FAILED_AUDIO_FRONTEND_INITIALIZATION;\n    }\n\n    if (status != Status::OK)\n    {\n        return status;\n    }\n\n    status = _set_up_midi(options, control_config);\n\n    auto midi_ok = _midi_frontend->init();\n    if (!midi_ok)\n    {\n        return Status::FAILED_MIDI_FRONTEND_INITIALIZATION;\n    }\n\n    _midi_dispatcher->set_frontend(_midi_frontend.get());\n\n    if (status != Status::OK)\n    {\n        return status;\n    }\n\n    _engine_controller = std::make_unique<engine::Controller>(_engine.get(),\n                                                              _midi_dispatcher.get(),\n                                                              _audio_frontend.get());\n\n    status = _set_up_control(options, configurator);\n    if (status != Status::OK)\n    {\n        return status;\n    }\n\n    return Status::OK;\n}\n\nStatus BaseFactory::_load_json_configuration(jsonconfig::JsonConfigurator* configurator)\n{\n    auto status = configurator->load_host_config();\n    if (status != jsonconfig::JsonConfigReturnStatus::OK)\n    {\n        return Status::FAILED_LOAD_HOST_CONFIG;\n    }\n\n    status = configurator->load_tracks();\n    if (status != jsonconfig::JsonConfigReturnStatus::OK)\n    {\n        return Status::FAILED_LOAD_TRACKS;\n    }\n\n    status = configurator->load_midi();\n    if (status != jsonconfig::JsonConfigReturnStatus::OK &&\n        status != jsonconfig::JsonConfigReturnStatus::NOT_DEFINED)\n    {\n        return Status::FAILED_LOAD_MIDI_MAPPING;\n    }\n\n    status = configurator->load_cv_gate();\n    if (status != jsonconfig::JsonConfigReturnStatus::OK &&\n        status != jsonconfig::JsonConfigReturnStatus::NOT_DEFINED)\n    {\n        return Status::FAILED_LOAD_CV_GATE;\n    }\n\n    status = configurator->load_initial_state();\n    if (status != jsonconfig::JsonConfigReturnStatus::OK &&\n        status != jsonconfig::JsonConfigReturnStatus::NOT_DEFINED)\n    {\n        return Status::FAILED_LOAD_PROCESSOR_STATES;\n    }\n\n    return Status::OK;\n}\n\nStatus BaseFactory::_set_up_control([[maybe_unused]] const SushiOptions& options,\n                                    [[maybe_unused]] jsonconfig::JsonConfigurator* configurator)\n{\n    if (options.use_osc)\n    {\n        auto oscpack_messenger = new osc::OscpackOscMessenger(options.osc_server_port,\n                                                              options.osc_send_port,\n                                                              options.osc_send_ip);\n\n        _osc_frontend = std::make_unique<control_frontend::OSCFrontend>(_engine.get(),\n                                                                        _engine_controller.get(),\n                                                                        oscpack_messenger);\n\n        _engine_controller->set_osc_frontend(_osc_frontend.get());\n\n        auto osc_status = _osc_frontend->init();\n        if (osc_status != control_frontend::ControlFrontendStatus::OK)\n        {\n            return Status::FAILED_OSC_FRONTEND_INITIALIZATION;\n        }\n\n        if (configurator)\n        {\n            configurator->set_osc_frontend(_osc_frontend.get());\n\n            auto status = configurator->load_osc();\n            if (status != jsonconfig::JsonConfigReturnStatus::OK &&\n                status != jsonconfig::JsonConfigReturnStatus::NOT_DEFINED)\n            {\n                return Status::FAILED_LOAD_OSC;\n            }\n        }\n    }\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n    if (options.use_grpc)\n    {\n        _rpc_server = std::make_unique<sushi_rpc::GrpcServer>(options.grpc_listening_address, _engine_controller.get());\n        ELKLOG_LOG_INFO(\"Instantiating gRPC server with address: {}\", options.grpc_listening_address);\n    }\n#endif\n\n    return Status::OK;\n}\n\n} // end namespace sushi::internal"
  },
  {
    "path": "src/factories/base_factory.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Public Base class for all Sushi factory implementations.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_FACTORY_BASE_H\n#define SUSHI_FACTORY_BASE_H\n\n#include \"sushi/sushi.h\"\n#include \"sushi/factory_interface.h\"\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n#include \"sushi_rpc/grpc_server.h\"\n#endif\n\nnamespace sushi::internal {\n\nnamespace engine {\nclass AudioEngine;\nclass Controller;\n}\n\nnamespace audio_frontend {\nstruct BaseAudioFrontendConfiguration;\nclass BaseAudioFrontend;\n}\n\nnamespace midi_frontend {\nclass BaseMidiFrontend;\n}\n\nnamespace midi_dispatcher {\nclass MidiDispatcher;\n}\n\nnamespace control_frontend {\nclass OSCFrontend;\n}\n\nnamespace jsonconfig {\nclass JsonConfigurator;\nstruct ControlConfig;\n}\n\n/**\n * @brief Virtual base class for Sushi Factories.\n *        This implements as much as possible that is in common for factories,\n *        leaving a numbed of virtual methods to be populated by sub-classes.\n *\n *        The design choice has been to not have any switch / if clauses here,\n *        constructing things differently depending on what frontend or other running\n *        configuration has been made.\n *\n *        Ownership of all subsystem is held by the factory until the point where a Sushi class\n *        is constructed and returned, at which point it takes over ownership.\n *\n *        Each factory instance is meant to be run only once and discarded.\n */\nclass BaseFactory : public FactoryInterface\n{\npublic:\n    BaseFactory();\n    ~BaseFactory() override;\n\nprotected:\n    std::unique_ptr<Sushi> _make_sushi();\n\n    void _instantiate_subsystems(SushiOptions& options);\n\n    Status _configure_from_file(SushiOptions& options);\n    Status _configure_from_json(SushiOptions& options);\n    Status _configure_with_defaults(SushiOptions& options);\n\n    Status _configure_timing_options(const SushiOptions& options);\n\n    Status _configure_engine(SushiOptions& options,\n                             const jsonconfig::ControlConfig& control_config,\n                             jsonconfig::JsonConfigurator* configurator);\n\n    static Status _load_json_configuration(jsonconfig::JsonConfigurator* configurator);\n\n    /**\n     * @brief Inherit and populate this to instantiate and configure the audio frontend.\n     * @param options A populated SushiOptions structure.\n     * @param config\n     * @return Status\n     */\n    virtual Status _setup_audio_frontend(const SushiOptions& options,\n                                         const jsonconfig::ControlConfig& config) = 0;\n\n    /**\n     * @brief Inherit and populate this to instantiate and configure the midi frontend.\n     * @param options A populated SushiOptions structure.\n     * @return Status\n     */\n    virtual Status _set_up_midi(const SushiOptions& options,\n                                const jsonconfig::ControlConfig& config) = 0;\n\n    /**\n     * @brief Handle sequenced events from configuration file here.\n     * @param options A populated SushiOptions structure.\n     * @param configurator\n     * @return Status\n     */\n    virtual Status _load_json_events(const SushiOptions& options,\n                                     jsonconfig::JsonConfigurator* configurator) = 0;\n\n    Status _status {Status::OK};\n\n    std::unique_ptr<engine::AudioEngine> _engine {nullptr};\n    std::unique_ptr<midi_dispatcher::MidiDispatcher> _midi_dispatcher {nullptr};\n\n    std::unique_ptr<midi_frontend::BaseMidiFrontend> _midi_frontend {nullptr};\n    std::unique_ptr<control_frontend::OSCFrontend> _osc_frontend {nullptr};\n    std::unique_ptr<audio_frontend::BaseAudioFrontend> _audio_frontend {nullptr};\n    std::unique_ptr<audio_frontend::BaseAudioFrontendConfiguration> _frontend_config {nullptr};\n\n    std::unique_ptr<engine::Controller> _engine_controller {nullptr};\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n    std::unique_ptr<sushi_rpc::GrpcServer> _rpc_server {nullptr};\n#endif\n\nprivate:\n    /**\n     * @brief This instantiates and configures gRPC, OSC, and eventual other future control.\n     *        It is common for all factories, since they share control functionality.\n     *        OSC / gRPC can instead be turned on/off using the option flags in SushiOptions.\n     * @param options A populated SushiOptions structure.\n     * @param configurator\n     * @return Status\n     */\n    Status _set_up_control(const SushiOptions& options,\n                           jsonconfig::JsonConfigurator* configurator);\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_FACTORY_BASE_H\n"
  },
  {
    "path": "src/factories/offline_factory.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Public Sushi factory for offline use.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"sushi/offline_factory.h\"\n\n#include \"offline_factory_implementation.h\"\n\nnamespace sushi {\n\nOfflineFactory::OfflineFactory()\n{\n    _implementation = std::make_unique<internal::OfflineFactoryImplementation>();\n}\n\nOfflineFactory::~OfflineFactory() = default;\n\nstd::pair<std::unique_ptr<Sushi>, Status> OfflineFactory::new_instance(SushiOptions& options)\n{\n    return _implementation->new_instance(options);\n}\n\n}"
  },
  {
    "path": "src/factories/offline_factory_implementation.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Concrete implementation of the Sushi factory for offline use.\n *        This is a PIMPL class of sorts, used inside offline_factory.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"offline_factory_implementation.h\"\n\n#include \"engine/audio_engine.h\"\n#include \"src/concrete_sushi.h\"\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n#include \"sushi_rpc/grpc_server.h\"\n#endif\n\n#include \"audio_frontends/offline_frontend.h\"\n\n#include \"engine/json_configurator.h\"\n\nnamespace sushi::internal {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"offline-factory\");\n\nOfflineFactoryImplementation::OfflineFactoryImplementation() = default;\n\nOfflineFactoryImplementation::~OfflineFactoryImplementation() = default;\n\nstd::pair<std::unique_ptr<Sushi>, Status> OfflineFactoryImplementation::new_instance(SushiOptions& options)\n{\n    // For the offline frontend, OSC control is not supported.\n    // So, the SushiOptions flag is overridden.\n    options.use_osc = false;\n\n#ifdef TWINE_BUILD_WITH_APPLE_COREAUDIO\n    ELKLOG_LOG_WARNING(\"Using the Offline frontend with more than 1 CPU core is not currently supported on Apple. \"\n                       \"The threads need to be attached to a workgroup, \"\n                       \"which will not exist if there is no audio interface. \"\n                       \"Sushi will proceed to run, but on a single CPU core.\");\n\n    options.rt_cpu_cores = 1;\n#endif\n\n    _instantiate_subsystems(options);\n\n    return {_make_sushi(), _status};\n}\n\nStatus OfflineFactoryImplementation::_setup_audio_frontend(const SushiOptions& options,\n                                                           const jsonconfig::ControlConfig& config)\n{\n    int cv_inputs = config.cv_inputs.value_or(0);\n    int cv_outputs = config.cv_outputs.value_or(0);\n\n    bool dummy = false;\n\n    if (options.frontend_type != FrontendType::OFFLINE)\n    {\n        dummy = true;\n        ELKLOG_LOG_INFO(\"Setting up dummy audio frontend\");\n    }\n    else\n    {\n        ELKLOG_LOG_INFO(\"Setting up offline audio frontend\");\n    }\n\n    _frontend_config = std::make_unique<audio_frontend::OfflineFrontendConfiguration>(options.input_filename,\n                                                                                      options.output_filename,\n                                                                                      dummy,\n                                                                                      cv_inputs,\n                                                                                      cv_outputs);\n\n    _audio_frontend = std::make_unique<audio_frontend::OfflineFrontend>(_engine.get());\n\n    return Status::OK;\n}\n\nStatus OfflineFactoryImplementation::_set_up_midi([[maybe_unused]] const SushiOptions& options,\n                                                  const jsonconfig::ControlConfig& config)\n{\n    int midi_inputs = config.midi_inputs.value_or(1);\n    int midi_outputs = config.midi_outputs.value_or(1);\n    _midi_dispatcher->set_midi_inputs(midi_inputs);\n    _midi_dispatcher->set_midi_outputs(midi_outputs);\n\n    _midi_frontend = std::make_unique<midi_frontend::NullMidiFrontend>(_midi_dispatcher.get());\n\n    return Status::OK;\n}\n\nStatus OfflineFactoryImplementation::_load_json_events([[maybe_unused]] const SushiOptions& options,\n                                                       jsonconfig::JsonConfigurator* configurator)\n{\n    auto [status, events] = configurator->load_event_list();\n\n    if (status == jsonconfig::JsonConfigReturnStatus::OK)\n    {\n        static_cast<audio_frontend::OfflineFrontend*>(_audio_frontend.get())->add_sequencer_events(std::move(events));\n    }\n    else if (status != jsonconfig::JsonConfigReturnStatus::NOT_DEFINED)\n    {\n        return Status::FAILED_LOAD_EVENT_LIST;\n    }\n\n    return Status::OK;\n}\n\n} // end namespace sushi::internal\n"
  },
  {
    "path": "src/factories/offline_factory_implementation.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Concrete implementation of the Sushi factory for offline use.\n *        This is a PIMPL class of sorts, used inside offline_factory.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_OFFLINE_FACTORY_H\n#define SUSHI_OFFLINE_FACTORY_H\n\n#include \"sushi/sushi.h\"\n\n#include \"base_factory.h\"\n\nnamespace sushi::internal {\n\n/**\n * @brief Factory for when Sushi is running in offline / dummy mode.\n */\nclass OfflineFactoryImplementation : public BaseFactory\n{\npublic:\n    OfflineFactoryImplementation();\n    ~OfflineFactoryImplementation() override;\n\n    std::pair<std::unique_ptr<Sushi>, Status> new_instance(SushiOptions& options) override;\n\nprotected:\n    Status _setup_audio_frontend(const SushiOptions& options,\n                                 const jsonconfig::ControlConfig& config) override;\n\n    Status _set_up_midi([[maybe_unused]] const SushiOptions& options,\n                        const jsonconfig::ControlConfig& config) override;\n\n    Status _load_json_events([[maybe_unused]] const SushiOptions& options,\n                             jsonconfig::JsonConfigurator* configurator) override;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_OFFLINE_FACTORY_H\n"
  },
  {
    "path": "src/factories/reactive_factory.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Public Sushi factory for reactive use.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"sushi/reactive_factory.h\"\n#include \"sushi/rt_controller.h\"\n\n#include \"reactive_factory_implementation.h\"\n\nnamespace sushi\n{\n\nReactiveFactory::ReactiveFactory()\n{\n    _implementation = std::make_unique<internal::ReactiveFactoryImplementation>();\n}\n\nReactiveFactory::~ReactiveFactory() = default;\n\nstd::pair<std::unique_ptr<Sushi>, Status> ReactiveFactory::new_instance(SushiOptions& options)\n{\n    return _implementation->new_instance(options);\n}\n\nstd::unique_ptr<RtController> ReactiveFactory::rt_controller()\n{\n    return _implementation->rt_controller();\n}\n\n}"
  },
  {
    "path": "src/factories/reactive_factory_implementation.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Concrete implementation of the Sushi factory for reactive use.\n *        This is a PIMPL class of sorts, used inside reactive_factory.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n#include \"sushi/utils.h\"\n\n#include \"reactive_factory_implementation.h\"\n\n#include \"audio_frontends/reactive_frontend.h\"\n#include \"control_frontends/reactive_midi_frontend.h\"\n#include \"engine/audio_engine.h\"\n\n#include \"src/concrete_sushi.h\"\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n#include \"sushi_rpc/grpc_server.h\"\n#endif\n\n#include \"engine/json_configurator.h\"\n\n#include \"engine/controller/real_time_controller.h\"\n#include \"control_frontends/oscpack_osc_messenger.h\"\n\nnamespace sushi::internal {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"reactive-factory\");\n\nReactiveFactoryImplementation::ReactiveFactoryImplementation() = default;\n\nReactiveFactoryImplementation::~ReactiveFactoryImplementation() = default;\n\nstd::pair<std::unique_ptr<Sushi>, Status> ReactiveFactoryImplementation::new_instance(SushiOptions& options)\n{\n    init_logger(options); // This can only be called once.\n\n    // Overriding whatever frontend choice may or may not have been set.\n    options.frontend_type = FrontendType::REACTIVE;\n\n    _instantiate_subsystems(options);\n\n    _real_time_controller = std::make_unique<RealTimeController>(\n        static_cast<audio_frontend::ReactiveFrontend*>(_audio_frontend.get()), // because -frtti is not used\n        static_cast<midi_frontend::ReactiveMidiFrontend*>(_midi_frontend.get()), // because -frtti is not used0\n        _engine->transport());\n\n    return {_make_sushi(), _status};\n}\n\nstd::unique_ptr<RtController> ReactiveFactoryImplementation::rt_controller()\n{\n    return std::move(_real_time_controller);\n}\n\nStatus ReactiveFactoryImplementation::_setup_audio_frontend([[maybe_unused]] const SushiOptions& options,\n                                                            const jsonconfig::ControlConfig& config)\n{\n    int cv_inputs = config.cv_inputs.value_or(0);\n    int cv_outputs = config.cv_outputs.value_or(0);\n\n    ELKLOG_LOG_INFO(\"Setting up reactive frontend\");\n    _frontend_config = std::make_unique<audio_frontend::ReactiveFrontendConfiguration>(cv_inputs, cv_outputs);\n\n    _audio_frontend = std::make_unique<audio_frontend::ReactiveFrontend>(_engine.get());\n\n    return Status::OK;\n}\n\nStatus ReactiveFactoryImplementation::_set_up_midi([[maybe_unused]] const SushiOptions& options,\n                                                   const jsonconfig::ControlConfig& config)\n{\n    // Will always be 1 & 1 for reactive frontend.\n    int midi_inputs = config.midi_inputs.value_or(1);\n    int midi_outputs = config.midi_outputs.value_or(1);\n    _midi_dispatcher->set_midi_inputs(midi_inputs);\n    _midi_dispatcher->set_midi_outputs(midi_outputs);\n\n    _midi_frontend = std::make_unique<midi_frontend::ReactiveMidiFrontend>(_midi_dispatcher.get());\n\n    return Status::OK;\n}\n\nStatus ReactiveFactoryImplementation::_load_json_events([[maybe_unused]] const SushiOptions& options,\n                                                        jsonconfig::JsonConfigurator* configurator)\n{\n    auto status = configurator->load_events();\n\n    if (status != jsonconfig::JsonConfigReturnStatus::OK &&\n        status != jsonconfig::JsonConfigReturnStatus::NOT_DEFINED)\n    {\n        return Status::FAILED_LOAD_EVENTS;\n    }\n\n    return Status::OK;\n}\n\n} // end namespace sushi::internal"
  },
  {
    "path": "src/factories/reactive_factory_implementation.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Concrete implementation of the Sushi factory for reactive use.\n *        This is a PIMPL class of sorts, used inside reactive_factory.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef REACTIVE_FACTORY_IMPLEMENTATION_H\n#define REACTIVE_FACTORY_IMPLEMENTATION_H\n\n#include \"sushi/rt_controller.h\"\n#include \"sushi/sushi.h\"\n\n#include \"base_factory.h\"\n\nnamespace sushi_rpc {\nclass GrpcServer;\n}\n\nnamespace sushi::internal {\n\nclass ConcreteSushi;\n\nnamespace audio_frontend {\nclass ReactiveFrontend;\n}\n\nnamespace midi_frontend {\nclass ReactiveMidiFrontend;\n}\n\nnamespace engine {\nclass Transport;\n}\n\n/**\n * @brief Factory for when Sushi will be embedded into another audio host or into a plugin,\n *        and will only use Reactive frontends for audio and MIDI.\n */\nclass ReactiveFactoryImplementation : public BaseFactory\n{\npublic:\n    ReactiveFactoryImplementation();\n    ~ReactiveFactoryImplementation() override;\n\n    std::pair<std::unique_ptr<Sushi>, Status> new_instance(SushiOptions& options) override;\n\n    /**\n     * @brief Returns an instance of a RealTimeController, if new_instance(...) completed successfully.\n     *        If not, it returns an empty unique_ptr.\n     * @return A unique_ptr with a RtController sub-class, or not, depending on InitStatus.\n     */\n    std::unique_ptr<RtController> rt_controller();\n\nprotected:\n    Status _setup_audio_frontend([[maybe_unused]] const SushiOptions& options,\n                                 const jsonconfig::ControlConfig& config) override;\n\n    Status _set_up_midi([[maybe_unused]] const SushiOptions& options,\n                        const jsonconfig::ControlConfig& config) override;\n\n    Status _load_json_events([[maybe_unused]] const SushiOptions& options,\n                             jsonconfig::JsonConfigurator* configurator) override;\n\nprivate:\n    std::unique_ptr<RtController> _real_time_controller {nullptr};\n};\n\n} // end namespace sushi::internal\n\n\n#endif // REACTIVE_FACTORY_IMPLEMENTATION_H\n"
  },
  {
    "path": "src/factories/standalone_factory.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Public Sushi factory for standalone use.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"sushi/standalone_factory.h\"\n\n#include \"standalone_factory_implementation.h\"\n\nnamespace sushi {\n\nStandaloneFactory::StandaloneFactory()\n{\n    _implementation = std::make_unique<internal::StandaloneFactoryImplementation>();\n}\n\nStandaloneFactory::~StandaloneFactory() = default;\n\nstd::pair<std::unique_ptr<Sushi>, Status> StandaloneFactory::new_instance(SushiOptions& options)\n{\n    return _implementation->new_instance(options);\n}\n\n}"
  },
  {
    "path": "src/factories/standalone_factory_implementation.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Concrete implementation of the Sushi factory for standalone use.\n *        This is a PIMPL class of sorts, used inside standalone_factory.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#define TWINE_EXPOSE_INTERNALS\n#include \"twine/twine.h\"\n\n#include \"elklog/static_logger.h\"\n\n#include \"standalone_factory_implementation.h\"\n\n#include \"engine/audio_engine.h\"\n#include \"src/concrete_sushi.h\"\n\n#ifdef SUSHI_BUILD_WITH_ALSA_MIDI\n#include \"control_frontends/alsa_midi_frontend.h\"\n#endif\n\n#ifdef SUSHI_BUILD_WITH_RT_MIDI\n#include \"control_frontends/rt_midi_frontend.h\"\n#endif\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n#include \"sushi_rpc/grpc_server.h\"\n#endif\n\n#include \"audio_frontends/jack_frontend.h\"\n#include \"audio_frontends/portaudio_frontend.h\"\n#include \"audio_frontends/xenomai_raspa_frontend.h\"\n\n#include \"engine/json_configurator.h\"\n#include \"control_frontends/oscpack_osc_messenger.h\"\n\n#include \"audio_frontends/apple_coreaudio_frontend.h\"\n\nnamespace sushi::internal {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"standalone-factory\");\n\nStandaloneFactoryImplementation::StandaloneFactoryImplementation() = default;\n\nStandaloneFactoryImplementation::~StandaloneFactoryImplementation() = default;\n\nstd::pair<std::unique_ptr<Sushi>, Status> StandaloneFactoryImplementation::new_instance(SushiOptions& options)\n{\n\n#ifdef SUSHI_BUILD_WITH_RASPA\n    auto raspa_status = audio_frontend::XenomaiRaspaFrontend::global_init();\n    if (raspa_status < 0)\n    {\n        _status = Status::FAILED_XENOMAI_INITIALIZATION;\n        return {nullptr, _status};\n    }\n\n    if (options.frontend_type == FrontendType::XENOMAI_RASPA)\n    {\n        twine::init_xenomai(); // must be called before setting up any worker pools\n    }\n#endif\n\n    _instantiate_subsystems(options);\n\n    return {_make_sushi(), _status};\n}\n\nStatus\n    StandaloneFactoryImplementation::_setup_audio_frontend(const SushiOptions& options,\n                                                           const jsonconfig::ControlConfig& config)\n{\n    int cv_inputs = config.cv_inputs.value_or(0);\n    int cv_outputs = config.cv_outputs.value_or(0);\n\n    switch (options.frontend_type)\n    {\n        case FrontendType::JACK:\n        {\n#ifdef SUSHI_BUILD_WITH_JACK\n            ELKLOG_LOG_INFO(\"Setting up Jack audio frontend\");\n            _frontend_config = std::make_unique<audio_frontend::JackFrontendConfiguration>(options.jack_client_name,\n                                                                                           options.jack_server_name,\n                                                                                           options.connect_ports,\n                                                                                           cv_inputs,\n                                                                                           cv_outputs);\n\n            _audio_frontend = std::make_unique<audio_frontend::JackFrontend>(_engine.get());\n#endif\n            break;\n        }\n        case FrontendType::PORTAUDIO:\n        {\n            ELKLOG_LOG_INFO(\"Setting up PortAudio frontend\");\n            _frontend_config = std::make_unique<audio_frontend::PortAudioFrontendConfiguration>(options.portaudio_input_device_id,\n                                                                                                options.portaudio_output_device_id,\n                                                                                                options.suggested_input_latency,\n                                                                                                options.suggested_output_latency,\n                                                                                                cv_inputs,\n                                                                                                cv_outputs);\n\n            _audio_frontend = std::make_unique<audio_frontend::PortAudioFrontend>(_engine.get());\n            break;\n        }\n\n        case FrontendType::APPLE_COREAUDIO:\n        {\n            ELKLOG_LOG_INFO(\"Setting up Apple CoreAudio frontend\");\n\n            _frontend_config = std::make_unique<audio_frontend::AppleCoreAudioFrontendConfiguration>(options.apple_coreaudio_input_device_uid,\n                                                                                                     options.apple_coreaudio_output_device_uid,\n                                                                                                     cv_inputs,\n                                                                                                     cv_outputs);\n\n            _audio_frontend = std::make_unique<audio_frontend::AppleCoreAudioFrontend>(_engine.get());\n            break;\n        }\n\n#ifdef SUSHI_BUILD_WITH_RASPA\n        case FrontendType::XENOMAI_RASPA:\n        {\n            ELKLOG_LOG_INFO(\"Setting up Xenomai RASPA frontend\");\n            _frontend_config = std::make_unique<audio_frontend::XenomaiRaspaFrontendConfiguration>(options.debug_mode_switches,\n                                                                                                   cv_inputs,\n                                                                                                   cv_outputs);\n\n            _audio_frontend = std::make_unique<audio_frontend::XenomaiRaspaFrontend>(_engine.get());\n            break;\n        }\n#endif\n        case FrontendType::DUMMY:\n        case FrontendType::OFFLINE:\n        {\n            ELKLOG_LOG_ERROR(\"Standalone Factory cannot be used to create DUMMY/OFFLINE frontends.\");\n            assert(false);\n            break;\n        }\n\n        default:\n        {\n            return Status::FAILED_AUDIO_FRONTEND_MISSING;\n        }\n    }\n\n    return Status::OK;\n}\n\nStatus StandaloneFactoryImplementation::_set_up_midi([[maybe_unused]] const SushiOptions& options,\n                                                     const jsonconfig::ControlConfig& config)\n{\n    int midi_inputs = config.midi_inputs.value_or(1);\n    int midi_outputs = config.midi_outputs.value_or(1);\n    _midi_dispatcher->set_midi_inputs(midi_inputs);\n    _midi_dispatcher->set_midi_outputs(midi_outputs);\n\n#ifdef SUSHI_BUILD_WITH_ALSA_MIDI\n    _midi_frontend = std::make_unique<midi_frontend::AlsaMidiFrontend>(midi_inputs,\n                                                                       midi_outputs,\n                                                                       _midi_dispatcher.get());\n#elif SUSHI_BUILD_WITH_RT_MIDI\n    auto rt_midi_input_mappings = config.rt_midi_input_mappings;\n    auto rt_midi_output_mappings = config.rt_midi_output_mappings;\n\n    _midi_frontend = std::make_unique<midi_frontend::RtMidiFrontend>(midi_inputs,\n                                                                     midi_outputs,\n                                                                     std::move(rt_midi_input_mappings),\n                                                                     std::move(rt_midi_output_mappings),\n                                                                     _midi_dispatcher.get());\n#else\n    _midi_frontend = std::make_unique<midi_frontend::NullMidiFrontend>(midi_inputs,\n                                                                       midi_outputs,\n                                                                       _midi_dispatcher.get());\n#endif\n\n    return Status::OK;\n}\n\nStatus StandaloneFactoryImplementation::_load_json_events([[maybe_unused]] const SushiOptions& options,\n                                                          jsonconfig::JsonConfigurator* configurator)\n{\n    auto status = configurator->load_events();\n\n    if (status != jsonconfig::JsonConfigReturnStatus::OK &&\n        status != jsonconfig::JsonConfigReturnStatus::NOT_DEFINED)\n    {\n        return Status::FAILED_LOAD_EVENTS;\n    }\n\n    return Status::OK;\n}\n\n} // end namespace sushi::internal\n"
  },
  {
    "path": "src/factories/standalone_factory_implementation.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Concrete implementation of the Sushi factory for standalone use.\n *        This is a PIMPL class of sorts, used inside standalone_factory.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_STANDALONE_FACTORY_IMPLEMENTATION_H\n#define SUSHI_STANDALONE_FACTORY_IMPLEMENTATION_H\n\n#include \"sushi/sushi.h\"\n\n#include \"base_factory.h\"\n\nnamespace sushi_rpc {\nclass GrpcServer;\n}\n\nnamespace sushi::internal {\n\n/**\n * @brief Factory for when Sushi will run in real-time standalone mode.\n */\nclass StandaloneFactoryImplementation : public BaseFactory\n{\npublic:\n    StandaloneFactoryImplementation();\n    ~StandaloneFactoryImplementation() override;\n\n    std::pair<std::unique_ptr<Sushi>, Status> new_instance(SushiOptions& options) override;\n\nprotected:\n    Status _setup_audio_frontend(const SushiOptions& options,\n                                 const jsonconfig::ControlConfig& config) override;\n\n    Status _set_up_midi([[maybe_unused]] const SushiOptions& options,\n                        const jsonconfig::ControlConfig& config) override;\n\n    Status _load_json_events([[maybe_unused]] const SushiOptions& options,\n                             jsonconfig::JsonConfigurator* configurator) override;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_STANDALONE_FACTORY_IMPLEMENTATION_H\n"
  },
  {
    "path": "src/library/base_performance_timer.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Measure processing performance\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_BASE_PERFORMANCE_TIMER_H\n#define SUSHI_BASE_PERFORMANCE_TIMER_H\n\n#include <optional>\n\nnamespace sushi::internal::performance {\n\nstruct ProcessTimings\n{\n    ProcessTimings() : avg_case{0.0f}, min_case{100.0f}, max_case{0.0f} {}\n    ProcessTimings(float avg, float min, float max) : avg_case{avg}, min_case{min}, max_case{max} {}\n    float avg_case{1};\n    float min_case{1};\n    float max_case{0};\n};\n\nclass BasePerformanceTimer\n{\npublic:\n    BasePerformanceTimer() = default;\n    virtual ~BasePerformanceTimer() = default;\n    /**\n    * @brief Set the period to use for timings\n    * @param timing_period The timing period in nanoseconds\n    */\n    virtual void set_timing_period(std::chrono::nanoseconds timing_period) = 0;\n\n    /**\n     * @brief Ser the period to use for timings implicitly\n     * @param samplerate The samplerate in Hz\n     * @param buffer_size The audio buffer size in samples\n     */\n    virtual void set_timing_period(float samplerate, int buffer_size) = 0;\n\n    /**\n     * @brief Enable or disable timings\n     * @param enabled Enable timings if true, disable if false\n     */\n    virtual void enable(bool enabled) = 0;\n\n    /**\n     * @brief Enable recording of every timing for a particular node\n     * @param node_id The node id to record\n     * @param enabled Enable timings if true, disable if false\n     */\n    virtual void enable_detailed_timings(int node_id, bool enabled) = 0;\n\n    /**\n     * @brief Query the enabled state\n     * @return True if the timer is enabled, false otherwise\n     */\n    virtual bool enabled() const = 0;\n\n    /**\n     * @brief List all enabled detailed timings\n     * @return A std::list of node ids for which detailed timing is enabled\n     */\n    virtual std::vector<int> enabled_detailed_timings() const = 0;\n\n    /**\n     * @brief Get the recorded timings from a specific node\n     * @param id An integer id representing a timing node\n     * @return A ProcessTimings object populated if the node has any timing records. Empty otherwise\n     */\n    virtual std::optional<ProcessTimings> timings_for_node(int id) = 0;\n\n    /**\n     * @brief Get all recorded timings from a specific node\n     * @param id An integer id representing a timing node\n     * @return A vector of all recorded timings if the node has any timing records. Empty otherwise\n     */\n    virtual const std::vector<std::chrono::nanoseconds>& detailed_timings_for_node(int id) = 0;\n\n    /**\n     * @brief Clear the recorded timings for a particular node\n     * @param id An integer id representing a timing node\n     * @return true if the node was found, false otherwise\n     */\n    virtual bool clear_timings_for_node(int id) = 0;\n\n    /**\n     * @brief Reset all recorded timings\n     */\n    virtual void clear_all_timings() = 0;\n};\n\n} // end namespace sushi::internal::performance\n\n#endif // SUSHI_BASE_PERFORMANCE_TIMER_H\n"
  },
  {
    "path": "src/library/base_processor_factory.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Abstract Factory for processors.\n *        Should be subclassed for each distinct Processor type, e.g. once for VST2, once for VST3, etc.\n */\n\n#ifndef SUSHI_BASE_PROCESSOR_FACTORY_H\n#define SUSHI_BASE_PROCESSOR_FACTORY_H\n\n#include \"library/processor.h\"\n#include \"engine/base_engine.h\"\n\nnamespace sushi::internal {\n\nclass BaseProcessorFactory\n{\npublic:\n    virtual ~BaseProcessorFactory() = default;\n\n    /**\n     * @brief Attempts to create a new Processor instance.\n     * @param plugin_info\n     * @param host_control\n     * @param sample_rate\n     * @return A pair with ReturnCode, and Processor instance.\n     *         If failed, the instance will be nullptr.\n     */\n    virtual std::pair<ProcessorReturnCode, std::shared_ptr<Processor>> new_instance(const PluginInfo& plugin_info,\n                                                                                    HostControl& host_control,\n                                                                                    float sample_rate) = 0;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_BASE_PROCESSOR_FACTORY_H\n"
  },
  {
    "path": "src/library/connection_types.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Datatypes and associated functions for managine audio and cv/cate connections\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_CONNECTION_TYPES_H\n#define SUSHI_CONNECTION_TYPES_H\n\n#include \"id_generator.h\"\n\nnamespace sushi::internal {\n\n/**\n * @brief Data for routing Audio to and from tracks\n */\nstruct AudioConnection\n{\n    int      engine_channel;\n    int      track_channel;\n    ObjectId track;\n};\n\n/**\n * @brief Data for routing control voltage to and from processor parameters\n */\nstruct CvConnection\n{\n    ObjectId processor_id;\n    ObjectId parameter_id;\n    int      cv_id;\n};\n\n/**\n * @brief Data for routing gate events to and from processors\n */\nstruct GateConnection\n{\n    ObjectId processor_id;\n    int      gate_id;\n    int      note_no;\n    int      channel;\n};\n\nbool inline operator==(const AudioConnection& lhs, const AudioConnection& rhs)\n{\n    return lhs.engine_channel == rhs.engine_channel &&\n           lhs.track_channel  == rhs.track_channel &&\n           lhs.track          == rhs.track;\n}\n\nbool inline operator==(const CvConnection& lhs, const CvConnection& rhs)\n{\n    return lhs.processor_id == rhs.processor_id &&\n           lhs.parameter_id == rhs.parameter_id &&\n           lhs.cv_id        == rhs.cv_id;\n}\n\nbool inline operator==(const GateConnection& lhs, const GateConnection& rhs)\n{\n    return lhs.processor_id == rhs.processor_id &&\n           lhs.gate_id      == rhs.gate_id &&\n           lhs.note_no      == rhs.note_no &&\n           lhs.channel      == rhs.channel;\n}\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_CONNECTION_TYPES_H\n\n"
  },
  {
    "path": "src/library/event.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Main event class used for communication across modules outside the rt part\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"twine/twine.h\"\n\n#include \"sushi/types.h\"\n\n#include \"library/event.h\"\n#include \"engine/base_engine.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\n\n/* GCC does not seem to get when a switch case handles all cases */\nELK_DISABLE_RETURN_TYPE\n\nnamespace sushi {\n\nRtDeletable::~RtDeletable()\n{\n    assert(twine::is_current_thread_realtime() == false);\n}\n\nnamespace internal {\n\n/**\n * Methods for real-time event classes:\n */\n\nstd::unique_ptr<Event> Event::from_rt_event(const RtEvent& rt_event, Time timestamp)\n{\n    switch (rt_event.type())\n    {\n        case RtEventType::NOTE_ON:\n        {\n            auto typed_ev = rt_event.keyboard_event();\n            return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::NOTE_ON,\n                                                   typed_ev->processor_id(),\n                                                   typed_ev->channel(),\n                                                   typed_ev->note(),\n                                                   typed_ev->velocity(),\n                                                   timestamp);\n        }\n        case RtEventType::NOTE_OFF:\n        {\n            auto typed_ev = rt_event.keyboard_event();\n            return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::NOTE_OFF,\n                                                   typed_ev->processor_id(),\n                                                   typed_ev->channel(),\n                                                   typed_ev->note(),\n                                                   typed_ev->velocity(),\n                                                   timestamp);\n        }\n        case RtEventType::NOTE_AFTERTOUCH:\n        {\n            auto typed_ev = rt_event.keyboard_event();\n            return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::NOTE_AFTERTOUCH,\n                                                   typed_ev->processor_id(),\n                                                   typed_ev->channel(),\n                                                   typed_ev->note(),\n                                                   typed_ev->velocity(),\n                                                   timestamp);\n        }\n        case RtEventType::MODULATION:\n        {\n            auto typed_ev = rt_event.keyboard_common_event();\n            return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::MODULATION,\n                                                   typed_ev->processor_id(),\n                                                   typed_ev->channel(),\n                                                   typed_ev->value(),\n                                                   timestamp);\n        }\n        case RtEventType::PITCH_BEND:\n        {\n            auto typed_ev = rt_event.keyboard_common_event();\n            return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::PITCH_BEND,\n                                                   typed_ev->processor_id(),\n                                                   typed_ev->channel(),\n                                                   typed_ev->value(),\n                                                   timestamp);\n        }\n        case RtEventType::AFTERTOUCH:\n        {\n            auto typed_ev = rt_event.keyboard_common_event();\n            return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::AFTERTOUCH,\n                                                   typed_ev->processor_id(),\n                                                   typed_ev->channel(),\n                                                   typed_ev->value(),\n                                                   timestamp);\n        }\n        case RtEventType::WRAPPED_MIDI_EVENT:\n        {\n            auto typed_ev = rt_event.wrapped_midi_event();\n            return std::make_unique<KeyboardEvent>(KeyboardEvent::Subtype::WRAPPED_MIDI,\n                                                   typed_ev->processor_id(),\n                                                   typed_ev->midi_data(),\n                                                   timestamp);\n        }\n        case RtEventType::TEMPO:\n        {\n            auto typed_event = rt_event.tempo_event();\n            return std::make_unique<TempoNotificationEvent>(typed_event->tempo(), timestamp);\n        }\n        case RtEventType::TIME_SIGNATURE:\n        {\n            auto typed_event = rt_event.time_signature_event();\n            return std::make_unique<TimeSignatureNotificationEvent>(typed_event->time_signature(), timestamp);\n        }\n        case RtEventType::PLAYING_MODE:\n        {\n            auto typed_event = rt_event.playing_mode_event();\n            return std::make_unique<PlayingModeNotificationEvent>(typed_event->mode(), timestamp);\n        }\n        case RtEventType::SYNC_MODE:\n        {\n            auto typed_event = rt_event.sync_mode_event();\n            return std::make_unique<SyncModeNotificationEvent>(typed_event->mode(), timestamp);\n        }\n        case RtEventType::ASYNC_WORK:\n        {\n            auto typed_ev = rt_event.async_work_event();\n            return std::make_unique<AsynchronousProcessorWorkEvent>(typed_ev->callback(),\n                                                                    typed_ev->callback_data(),\n                                                                    typed_ev->processor_id(),\n                                                                    typed_ev->event_id(),\n                                                                    timestamp);\n        }\n        case RtEventType::BLOB_DELETE:\n        {\n            auto typed_ev = rt_event.data_payload_event();\n            return std::make_unique<AsynchronousBlobDeleteEvent>(typed_ev->value(), timestamp);\n        }\n        case RtEventType::CLIP_NOTIFICATION:\n        {\n            auto typed_ev = rt_event.clip_notification_event();\n            auto channel_type = typed_ev->channel_type() == ClipNotificationRtEvent::ClipChannelType::INPUT?\n                                                            ClippingNotificationEvent::ClipChannelType::INPUT :\n                                                            ClippingNotificationEvent::ClipChannelType::OUTPUT;\n            return std::make_unique<ClippingNotificationEvent>(typed_ev->channel(), channel_type, timestamp);\n        }\n        case RtEventType::DELETE:\n        {\n            auto typed_ev = rt_event.delete_data_event();\n            return std::make_unique<AsynchronousDeleteEvent>(typed_ev->data(), timestamp);\n        }\n        case RtEventType::NOTIFY:\n        {\n            auto typed_ev = rt_event.processor_notify_event();\n            if (typed_ev->action() == ProcessorNotifyRtEvent::Action::PARAMETER_UPDATE)\n            {\n                return std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_UPDATED,\n                                                                     typed_ev->processor_id(),\n                                                                     0,\n                                                                     timestamp);\n            }\n            break;\n        }\n        case RtEventType::TIMING_TICK:\n        {\n            auto typed_ev = rt_event.timing_tick_event();\n            return std::make_unique<EngineTimingTickNotificationEvent>(typed_ev->tick_count(), timestamp);\n        }\n        default:\n            return nullptr;\n    }\n\n    assert(false);\n    return nullptr;\n}\n\nRtEvent KeyboardEvent::to_rt_event(int sample_offset) const\n{\n    switch (_subtype)\n    {\n        case KeyboardEvent::Subtype::NOTE_ON:\n            return RtEvent::make_note_on_event(_processor_id, sample_offset, _channel, _note, _velocity);\n\n        case KeyboardEvent::Subtype::NOTE_OFF:\n            return RtEvent::make_note_off_event(_processor_id, sample_offset, _channel, _note, _velocity);\n\n        case KeyboardEvent::Subtype::NOTE_AFTERTOUCH:\n            return RtEvent::make_note_aftertouch_event(_processor_id, sample_offset, _channel, _note, _velocity);\n\n        case KeyboardEvent::Subtype::AFTERTOUCH:\n            return RtEvent::make_aftertouch_event(_processor_id, sample_offset, _channel, _velocity);\n\n        case KeyboardEvent::Subtype::PITCH_BEND:\n            return RtEvent::make_pitch_bend_event(_processor_id, sample_offset, _channel, _velocity);\n\n        case KeyboardEvent::Subtype::MODULATION:\n            return RtEvent::make_kb_modulation_event(_processor_id, sample_offset, _channel, _velocity);\n\n        case KeyboardEvent::Subtype::WRAPPED_MIDI:\n            return RtEvent::make_wrapped_midi_event(_processor_id, sample_offset, _midi_data);\n    }\n\n    assert(false);\n    return RtEvent {};\n}\n\nRtEvent ParameterChangeEvent::to_rt_event(int sample_offset) const\n{\n    switch (_subtype)\n    {\n        case ParameterChangeEvent::Subtype::INT_PARAMETER_CHANGE:\n            return RtEvent::make_parameter_change_event(_processor_id,\n                                                        sample_offset,\n                                                        _parameter_id,\n                                                        static_cast<float>(this->int_value()));\n\n        case ParameterChangeEvent::Subtype::FLOAT_PARAMETER_CHANGE:\n            return RtEvent::make_parameter_change_event(_processor_id,\n                                                        sample_offset,\n                                                        _parameter_id,\n                                                        this->float_value());\n\n        case ParameterChangeEvent::Subtype::BOOL_PARAMETER_CHANGE:\n            return RtEvent::make_parameter_change_event(_processor_id,\n                                                        sample_offset,\n                                                        _parameter_id,\n                                                        static_cast<float>(this->bool_value()));\n    }\n\n    /* Only to stop the compiler from complaining */\n    return {};\n}\n\nRtEvent SetProcessorBypassEvent::to_rt_event(int /*sample_offset*/) const\n{\n    return RtEvent::make_bypass_processor_event(this->processor_id(), this->bypass_enabled());\n}\n\nRtEvent RtStateEvent::to_rt_event(int /*sample_offset*/) const\n{\n    // If this is null, then this object has been converted to an RtEvent before, which would imply a larger bug\n    assert(_state);\n    return RtEvent::make_set_rt_state_event(_processor_id, _state.release());\n}\n\nRtStateEvent::RtStateEvent(ObjectId processor_id, std::unique_ptr<RtState> state, Time timestamp) : Event(timestamp),\n                                                                                                    _processor_id(processor_id),\n                                                                                                    _state(std::move(state))\n{}\n\nRtStateEvent::~RtStateEvent() = default;\n\nRtEvent DataPropertyEvent::to_rt_event(int sample_offset) const\n{\n    return RtEvent::make_data_property_change_event(_processor_id, sample_offset, _property_id, _blob_value);\n}\n\nRtEvent StringPropertyEvent::to_rt_event(int sample_offset) const\n{\n    /* std::string is a too large and complex type to be copied by value into an RtEvent.\n     * Instead, copy the string to a heap allocation that will outlive the event.\n     * The string should be taken back to the non-rt domain and deleted there. This is handled\n     * automatically by InternalPlugins process_event() function */\n    auto heap_string = new RtDeletableWrapper<std::string>(_string_value);\n    return RtEvent::make_string_property_change_event(_processor_id, sample_offset, _property_id, heap_string);\n}\n\nRtEvent AsynchronousProcessorWorkCompletionEvent::to_rt_event(int /*sample_offset*/) const\n{\n    return RtEvent::make_async_work_completion_event(_rt_processor, _rt_event_id, _return_value);\n}\n\n/**\n * Methods for asynchronous processing event classes:\n */\n\nstd::unique_ptr<Event> AsynchronousProcessorWorkEvent::execute()\n{\n    int status = _work_callback(_data, _rt_event_id);\n    return std::make_unique<AsynchronousProcessorWorkCompletionEvent>(status, _rt_processor, _rt_event_id, IMMEDIATE_PROCESS);\n}\n\nstd::unique_ptr<Event> AsynchronousBlobDeleteEvent::execute()\n{\n    delete(_data.data);\n    \n    return nullptr;\n}\n\nstd::unique_ptr<Event> AsynchronousDeleteEvent::execute()\n{\n    delete _data;\n\n    return nullptr;\n}\n\nint ProgramChangeEvent::execute(engine::BaseEngine* engine) const\n{\n    auto processor = engine->processor_container()->mutable_processor(_processor_id);\n    if (processor != nullptr)\n    {\n        auto status = processor->set_program(_program_no);\n        if (status == ProcessorReturnCode::OK)\n        {\n            return EventStatus::HANDLED_OK;\n        }\n    }\n    return EventStatus::NOT_HANDLED;\n}\n\nint PropertyChangeEvent::execute(engine::BaseEngine* engine) const\n{\n    auto processor = engine->processor_container()->mutable_processor(_processor_id);\n    if (processor != nullptr)\n    {\n        auto status = processor->set_property_value(_property_id, _string_value);\n        if (status == ProcessorReturnCode::OK)\n        {\n            return EventStatus::HANDLED_OK;\n        }\n    }\n    return EventStatus::NOT_HANDLED;\n}\n\nint SetEngineTempoEvent::execute(engine::BaseEngine* engine) const\n{\n    engine->set_tempo(_tempo);\n    return 0;\n}\n\nint SetEngineTimeSignatureEvent::execute(engine::BaseEngine* engine) const\n{\n    engine->set_time_signature(_signature);\n    return 0;\n}\n\nint SetEnginePlayingModeStateEvent::execute(engine::BaseEngine* engine) const\n{\n    engine->set_transport_mode(_mode);\n    return 0;\n}\n\nint SetEngineSyncModeEvent::execute(engine::BaseEngine* engine) const\n{\n    engine->set_tempo_sync_mode(_mode);\n    return 0;\n}\n\nELK_POP_WARNING\n\n} // end namespace internal\n\n} // end namespace sushi::internal\n"
  },
  {
    "path": "src/library/event.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Main event class used for communication across modules outside the rt part\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_CONTROL_EVENT_H\n#define SUSHI_CONTROL_EVENT_H\n\n#include <string>\n#include <memory>\n#include <vector>\n\n#include \"sushi/sushi_time.h\"\n#include \"sushi/types.h\"\n\n#include \"id_generator.h\"\n#include \"library/rt_event.h\"\n#include \"base_performance_timer.h\"\n\nnamespace sushi::internal {\n\nnamespace engine {class BaseEngine;}\n\nnamespace dispatcher\n{\n    class EventDispatcher;\n    class Worker;\n}\n\nclass Event;\n\n/* This is a weakly typed enum to allow for an opaque communication channel\n * between Receivers and avoid having to define all possible values inside\n * the event class header */\nnamespace EventStatus {\nenum EventStatus : int\n{\n    HANDLED_OK,\n    ERROR,\n    NOT_HANDLED,\n    QUEUED_HANDLING,\n    UNRECOGNIZED_RECEIVER,\n    UNRECOGNIZED_EVENT,\n    EVENT_SPECIFIC\n};\n}\n\ntypedef void (*EventCompletionCallback)(void *arg, Event* event, int status);\n\n/**\n * @brief Event baseclass\n */\nclass Event\n{\n    friend class sushi::internal::dispatcher::EventDispatcher;\n    friend class sushi::internal::dispatcher::Worker;\n\npublic:\n    virtual ~Event() = default;\n\n    /**\n     * @brief Creates an Event from its RtEvent counterpart if possible\n     * @param rt_event The RtEvent to convert from\n     * @param timestamp the timestamp to assign to the Event\n     * @return pointer to an Event if successful, nullptr if there is no possible conversion\n     */\n    static std::unique_ptr<Event> from_rt_event(const RtEvent& rt_event, Time timestamp);\n\n    [[nodiscard]] Time        time() const {return _timestamp;}\n    [[nodiscard]] int         receiver() const {return _receiver;}\n    [[nodiscard]] EventId     id() const {return _id;}\n\n    /**\n     * @brief Set a callback function that will be called after the event has been handled\n     * @param callback A function pointer that will be called on completion\n     * @param data Data that will be passed as function argument\n     */\n    void set_completion_cb(EventCompletionCallback callback, void* data)\n    {\n        _completion_cb = callback;\n        _callback_arg = data;\n    }\n\n    /**\n     * @brief Whether the events should be processes asynchronously in a low priority thread or not\n     * @return true if the Event should be processed asynchronously, false otherwise.\n     */\n    [[nodiscard]] virtual bool process_asynchronously() const {return false;}\n\n    /**\n     * Events that are directly convertible to an RtEvent.\n     */\n    [[nodiscard]] virtual bool maps_to_rt_event() const {return false;}\n\n    /**\n     * Return the RtEvent counterpart of the Event\n     */\n    [[nodiscard]] virtual RtEvent to_rt_event(int /*sample_offset*/) const {return {};}\n\n    /**\n     * The below should all be overridden only by one single sub-class.\n     * They are only used for reflection:\n     *  to statically cast to a particular sub-class of Event.\n     */\n\n    /* Convertible to KeyboardEvent */\n    [[nodiscard]] virtual bool is_keyboard_event() const {return false;}\n\n    /* Convertible to ParameterChangeNotification */\n    [[nodiscard]] virtual bool is_parameter_change_event() const {return false;}\n\n    /* Convertible to ParameterChangeNotification */\n    [[nodiscard]] virtual bool is_parameter_change_notification() const {return false;}\n\n    /* Convertible to PropertyChangeNotification */\n    [[nodiscard]] virtual bool is_property_change_notification() const {return false;}\n\n    /* Convertible to EngineEvent */\n    [[nodiscard]] virtual bool is_engine_event() const {return false;}\n\n    /* Convertible to EngineNotificationEvent */\n    [[nodiscard]] virtual bool is_engine_notification() const {return false;}\n\n    /* Convertible to AsynchronousWorkEvent */\n    [[nodiscard]] virtual bool is_async_work_event() const {return false;}\n\nprotected:\n    explicit Event(Time timestamp) : _timestamp(timestamp) {}\n\n    /* Only the dispatcher can set the receiver and call the completion callback */\n\n    [[nodiscard]] EventCompletionCallback completion_cb() const {return _completion_cb;}\n    [[nodiscard]] void*                   callback_arg() const {return _callback_arg;}\n\nprivate:\n    int                     _receiver{0};\n    Time                    _timestamp;\n    EventCompletionCallback _completion_cb{nullptr};\n    void*                   _callback_arg{nullptr};\n    EventId                 _id{EventIdGenerator::new_id()};\n};\n\n/**\n * The subsequent events all map to real-time events:\n */\n\nclass KeyboardEvent : public Event\n{\npublic:\n    enum class Subtype\n    {\n        NOTE_ON,\n        NOTE_OFF,\n        NOTE_AFTERTOUCH,\n        AFTERTOUCH,\n        PITCH_BEND,\n        MODULATION,\n        WRAPPED_MIDI\n    };\n\n    KeyboardEvent(Subtype subtype,\n                  ObjectId processor_id,\n                  int channel,\n                  float value,\n                  Time timestamp) : Event(timestamp),\n                                    _subtype(subtype),\n                                    _processor_id(processor_id),\n                                    _channel(channel),\n                                    _note(0),\n                                    _velocity(value)\n    {\n        assert(_subtype == Subtype::AFTERTOUCH ||\n               _subtype == Subtype::PITCH_BEND ||\n               _subtype == Subtype::MODULATION);\n    }\n\n    KeyboardEvent(Subtype subtype,\n                  ObjectId processor_id,\n                  int channel,\n                  int note,\n                  float velocity,\n                  Time timestamp) : Event(timestamp),\n                                    _subtype(subtype),\n                                    _processor_id(processor_id),\n                                    _channel(channel),\n                                    _note(note),\n                                    _velocity(velocity) {}\n\n    KeyboardEvent(Subtype subtype,\n                  ObjectId processor_id,\n                  MidiDataByte midi_data,\n                  Time timestamp) : Event(timestamp),\n                                    _subtype(subtype),\n                                    _processor_id(processor_id),\n                                    _midi_data(midi_data) {}\n\n    bool is_keyboard_event() const override {return true;}\n\n    bool maps_to_rt_event() const override {return true;}\n\n    [[nodiscard]] RtEvent to_rt_event(int sample_offset) const override;\n\n    Subtype       subtype() const {return _subtype;}\n    [[nodiscard]] ObjectId processor_id() const {return _processor_id;}\n    [[nodiscard]] int      channel() const {return _channel;}\n    [[nodiscard]] int      note() const {return _note;}\n    [[nodiscard]] float    velocity() const {return _velocity;}\n    [[nodiscard]] float    value() const {return _velocity;}\n    MidiDataByte midi_data() {return _midi_data;}\n\nprivate:\n    Subtype         _subtype;\n    ObjectId        _processor_id;\n    int             _channel;\n    int             _note;\n    float           _velocity;\n    MidiDataByte    _midi_data;\n};\n\nclass ParameterChangeEvent : public Event\n{\npublic:\n    enum class Subtype\n    {\n        BOOL_PARAMETER_CHANGE,\n        INT_PARAMETER_CHANGE,\n        FLOAT_PARAMETER_CHANGE\n    };\n\n    ParameterChangeEvent(Subtype subtype,\n                         ObjectId processor_id,\n                         ObjectId parameter_id,\n                         float value,\n                         Time timestamp) : Event(timestamp),\n                                           _subtype(subtype),\n                                           _processor_id(processor_id),\n                                           _parameter_id(parameter_id),\n                                           _value(value) {}\n\n    [[nodiscard]] bool maps_to_rt_event() const override {return true;}\n\n    [[nodiscard]] RtEvent to_rt_event(int sample_offset) const override;\n\n    [[nodiscard]] bool is_parameter_change_event() const override {return true;}\n\n    [[nodiscard]] Subtype             subtype() const {return _subtype;}\n    [[nodiscard]] ObjectId            processor_id() const {return _processor_id;}\n    [[nodiscard]] ObjectId            parameter_id() const {return _parameter_id;}\n    [[nodiscard]] float               float_value() const {return _value;}\n    [[nodiscard]] int                 int_value() const {return static_cast<int>(_value);}\n    [[nodiscard]] bool                bool_value() const {return _value > 0.5f;}\n\nprivate:\n    Subtype             _subtype;\n\nprotected:\n    ObjectId            _processor_id;\n    ObjectId            _parameter_id;\n    float               _value;\n};\n\nclass DataPropertyEvent : public Event\n{\npublic:\n    DataPropertyEvent(ObjectId processor_id,\n                      int property_id,\n                      BlobData blob_value,\n                      Time timestamp) : Event(timestamp),\n                                        _processor_id(processor_id),\n                                        _property_id(property_id),\n                                        _blob_value(blob_value) {}\n\n    [[nodiscard]] bool maps_to_rt_event() const override {return true;}\n\n    [[nodiscard]] RtEvent to_rt_event(int sample_offset) const override;\n\nprivate:\n    ObjectId _processor_id;\n    int      _property_id;\n    BlobData _blob_value;\n};\n\nclass StringPropertyEvent : public Event\n{\npublic:\n    StringPropertyEvent(ObjectId processor_id,\n                       int property_id,\n                       const std::string& string_value,\n                       Time timestamp) : Event(timestamp),\n                                        _processor_id(processor_id),\n                                        _property_id(property_id),\n                                        _string_value(string_value) {}\n\n    [[nodiscard]] bool maps_to_rt_event() const override {return true;}\n\n    [[nodiscard]] RtEvent to_rt_event(int sample_offset) const override;\n\nprivate:\n    ObjectId    _processor_id;\n    ObjectId    _property_id;\n    std::string _string_value;\n};\n\nclass RtStateEvent : public Event\n{\npublic:\n    RtStateEvent(ObjectId processor_id,\n                 std::unique_ptr<RtState> state,\n                 Time timestamp);\n\n    ~RtStateEvent() override;\n\n    bool maps_to_rt_event() const override {return true;}\n\n    RtEvent to_rt_event(int sample_offset) const override;\n\nprivate:\n    ObjectId _processor_id;\n    mutable std::unique_ptr<RtState> _state;\n};\n\nclass SetProcessorBypassEvent : public Event\n{\npublic:\n    SetProcessorBypassEvent(ObjectId processor_id, bool bypass_enabled, Time timestamp) : Event(timestamp),\n                                                                                          _processor_id(processor_id),\n                                                                                          _bypass_enabled(bypass_enabled)\n    {}\n\n    [[nodiscard]] bool maps_to_rt_event() const override {return true;}\n\n    [[nodiscard]] RtEvent to_rt_event(int sample_offset) const override;\n\n    [[nodiscard]] ObjectId processor_id() const {return _processor_id;}\n    [[nodiscard]] bool bypass_enabled() const {return _bypass_enabled;}\n\nprivate:\n    ObjectId _processor_id;\n    bool     _bypass_enabled;\n};\n\nclass AsynchronousProcessorWorkCompletionEvent : public Event\n{\npublic:\n    AsynchronousProcessorWorkCompletionEvent(int return_value,\n                                             ObjectId processor,\n                                             EventId rt_event_id,\n\n                                             Time timestamp) : Event(timestamp),\n                                                               _return_value(return_value),\n                                                               _rt_processor(processor),\n                                                               _rt_event_id(rt_event_id) {}\n\n    [[nodiscard]] bool maps_to_rt_event() const override {return true;}\n    [[nodiscard]] RtEvent to_rt_event(int sample_offset) const override;\n\nprivate:\n    int         _return_value;\n    ObjectId    _rt_processor;\n    EventId     _rt_event_id;\n};\n\n/**\n * The following events are all for Asynchronous processing:\n */\n\nclass EngineEvent : public Event\n{\npublic:\n    [[nodiscard]] bool process_asynchronously() const override {return true;}\n\n    [[nodiscard]] bool is_engine_event() const override {return true;}\n\n    [[nodiscard]] virtual int execute(engine::BaseEngine* engine) const = 0;\n\nprotected:\n    explicit EngineEvent(Time timestamp) : Event(timestamp) {}\n};\n\ntemplate <typename LambdaType>\nclass LambdaEvent : public EngineEvent\n{\npublic:\n    LambdaEvent(const LambdaType& work_lambda,\n                Time timestamp) : EngineEvent(timestamp),\n                                  _work_lambda(work_lambda) {}\n\n    LambdaEvent(LambdaType&& work_lambda,\n                Time timestamp) : EngineEvent(timestamp),\n                                  _work_lambda(std::move(work_lambda)) {}\n\n    int execute(engine::BaseEngine*) const override\n    {\n        return _work_lambda();\n    }\n\nprotected:\n    LambdaType _work_lambda;\n};\n\nclass ProgramChangeEvent : public EngineEvent\n{\npublic:\n    ProgramChangeEvent(ObjectId processor_id,\n                       int program_no,\n                       Time timestamp) : EngineEvent(timestamp),\n                                         _processor_id(processor_id),\n                                         _program_no(program_no) {}\n\n    int execute(engine::BaseEngine* engine) const override;\n\n    [[nodiscard]] ObjectId processor_id() const {return _processor_id;}\n    [[nodiscard]] int      program_no() const {return _program_no;}\n\nprivate:\n    ObjectId            _processor_id;\n    int                 _program_no;\n};\n\nclass PropertyChangeEvent : public EngineEvent\n{\npublic:\n    PropertyChangeEvent(ObjectId processor_id,\n                        ObjectId property_id,\n                        std::string  string_value,\n                        Time timestamp) : EngineEvent(timestamp),\n                                          _processor_id(processor_id),\n                                          _property_id(property_id),\n                                          _string_value(std::move(string_value)) {}\n\n    int execute(engine::BaseEngine* engine) const override;\n\nprivate:\n    ObjectId    _processor_id;\n    ObjectId    _property_id;\n    std::string _string_value;\n};\n\nclass SetEngineTempoEvent : public EngineEvent\n{\npublic:\n    SetEngineTempoEvent(float tempo, Time timestamp) : EngineEvent(timestamp),\n                                                       _tempo(tempo) {}\n\n    int execute(engine::BaseEngine* engine) const override;\n\nprivate:\n    float _tempo;\n};\n\nclass SetEngineTimeSignatureEvent : public EngineEvent\n{\npublic:\n    SetEngineTimeSignatureEvent(TimeSignature signature, Time timestamp) : EngineEvent(timestamp),\n                                                                           _signature(signature) {}\n\n    int execute(engine::BaseEngine* engine) const override;\n\nprivate:\n    TimeSignature _signature;\n};\n\nclass SetEnginePlayingModeStateEvent : public EngineEvent\n{\npublic:\n    SetEnginePlayingModeStateEvent(PlayingMode mode, Time timestamp) : EngineEvent(timestamp),\n                                                                       _mode(mode) {}\n\n    int execute(engine::BaseEngine* engine) const override;\n\nprivate:\n    PlayingMode _mode;\n};\n\nclass SetEngineSyncModeEvent : public EngineEvent\n{\npublic:\n    SetEngineSyncModeEvent(SyncMode mode, Time timestamp) : EngineEvent(timestamp),\n                                                            _mode(mode) {}\n\n    int execute(engine::BaseEngine* engine) const override;\n\nprivate:\n    SyncMode _mode;\n};\n\nclass AsynchronousWorkEvent : public Event\n{\npublic:\n    [[nodiscard]] bool process_asynchronously() const override {return true;}\n    [[nodiscard]] bool is_async_work_event() const override {return true;}\n    virtual std::unique_ptr<Event> execute() = 0;\n\nprotected:\n    explicit AsynchronousWorkEvent(Time timestamp) : Event(timestamp) {}\n};\n\ntypedef int (*AsynchronousWorkCallback)(void* data, EventId id);\n\nclass AsynchronousProcessorWorkEvent : public AsynchronousWorkEvent\n{\npublic:\n    AsynchronousProcessorWorkEvent(AsynchronousWorkCallback callback,\n                                   void* data,\n                                   ObjectId processor,\n                                   EventId rt_event_id,\n                                   Time timestamp) : AsynchronousWorkEvent(timestamp),\n                                                     _work_callback(callback),\n                                                     _data(data),\n                                                     _rt_processor(processor),\n                                                     _rt_event_id(rt_event_id)\n    {}\n\n    std::unique_ptr<Event> execute() override;\n\nprivate:\n    AsynchronousWorkCallback _work_callback;\n    void*                    _data;\n    ObjectId                 _rt_processor;\n    EventId                  _rt_event_id;\n};\n\nclass AsynchronousBlobDeleteEvent : public AsynchronousWorkEvent\n{\npublic:\n    AsynchronousBlobDeleteEvent(BlobData data,\n                                Time timestamp) : AsynchronousWorkEvent(timestamp),\n                                                  _data(data) {}\n    std::unique_ptr<Event> execute() override;\n\nprivate:\n    BlobData _data;\n};\n\nclass AsynchronousDeleteEvent : public AsynchronousWorkEvent\n{\npublic:\n    AsynchronousDeleteEvent(RtDeletable* data,\n                            Time timestamp) : AsynchronousWorkEvent(timestamp),\n                                              _data(data) {}\n    std::unique_ptr<Event> execute() override;\n\nprivate:\n    RtDeletable* _data;\n};\n\n/**\n * The subsequent events are processed immediately, in the non-real-time thread.\n * They do not map to real-time events, nor are they processed asynchronously.\n */\n\nclass ParameterChangeNotificationEvent : public Event\n{\npublic:\n    ParameterChangeNotificationEvent(ObjectId processor_id,\n                                     ObjectId parameter_id,\n                                     float normalized_value,\n                                     float domain_value,\n                                     std::string formatted_value,\n                                     Time timestamp) : Event(timestamp),\n                                                       _processor_id(processor_id),\n                                                       _parameter_id(parameter_id),\n                                                       _normalized_value(normalized_value),\n                                                       _domain_value(domain_value),\n                                                       _formatted_value(std::move(formatted_value)) {}\n\n    [[nodiscard]] bool is_parameter_change_notification() const override {return true;}\n\n    [[nodiscard]] ObjectId processor_id() const {return _processor_id;}\n\n    [[nodiscard]] ObjectId parameter_id() const {return _parameter_id;}\n\n    [[nodiscard]] float normalized_value() const {return _normalized_value;}\n\n    [[nodiscard]] float domain_value() const {return _domain_value;}\n\n    [[nodiscard]] const std::string& formatted_value() const {return _formatted_value;}\n\nprivate:\n    ObjectId    _processor_id;\n    ObjectId    _parameter_id;\n    float       _normalized_value;\n    float       _domain_value;\n    std::string _formatted_value;\n};\n\nclass PropertyChangeNotificationEvent : public Event\n{\npublic:\n    PropertyChangeNotificationEvent(ObjectId processor_id,\n                                    ObjectId property_id,\n                                    std::string value,\n                                    Time timestamp) : Event(timestamp),\n                                                      _processor_id(processor_id),\n                                                      _property_id(property_id),\n                                                      _value(std::move(value)) {}\n\n    [[nodiscard]] bool is_property_change_notification() const override {return true;}\n\n    [[nodiscard]] ObjectId processor_id() const {return _processor_id;}\n\n    [[nodiscard]] ObjectId property_id() const {return _property_id;}\n\n    [[nodiscard]] const std::string& value() const {return _value;}\n\nprivate:\n    ObjectId    _processor_id;\n    ObjectId    _property_id;\n    std::string _value;\n};\n\nclass EngineNotificationEvent : public Event\n{\npublic:\n    [[nodiscard]] bool is_engine_notification() const override {return true;}\n\n    /* Convertible to ClippingNotification */\n    [[nodiscard]] virtual bool is_clipping_notification() const {return false;}\n\n    /* Convertible to AudioGraphNotification */\n    [[nodiscard]] virtual bool is_audio_graph_notification() const {return false;}\n\n    /* Convertible to TempoNotification */\n    [[nodiscard]] virtual bool is_tempo_notification() const {return false;}\n\n    /* Convertible to TimeSignatureNotification */\n    [[nodiscard]] virtual bool is_time_sign_notification() const {return false;}\n\n    /* Convertible to PlayingModeNotification */\n    [[nodiscard]] virtual bool is_playing_mode_notification() const {return false;}\n\n    /* Convertible to SyncModeNotification */\n    [[nodiscard]] virtual bool is_sync_mode_notification() const {return false;}\n\n    /* Convertible to TimingNotification */\n    [[nodiscard]] virtual bool is_timing_notification() const {return false;}\n\n    /* Convertible to TimingTickNotification */\n    [[nodiscard]] virtual bool is_timing_tick_notification() const {return false;}\n\nprotected:\n    EngineNotificationEvent(Time timestamp) : Event(timestamp) {}\n};\n\nclass ClippingNotificationEvent : public EngineNotificationEvent\n{\npublic:\n    enum class ClipChannelType\n    {\n        INPUT,\n        OUTPUT,\n    };\n    ClippingNotificationEvent(int channel,\n                              ClipChannelType channel_type,\n                              Time timestamp) : EngineNotificationEvent(timestamp),\n                                                                                           _channel(channel),\n                                                                                           _channel_type(channel_type) {}\n    bool            is_clipping_notification() const override {return true;}\n    int             channel() const {return _channel;}\n    ClipChannelType channel_type() const {return _channel_type;}\n\nprivate:\n    int             _channel;\n    ClipChannelType _channel_type;\n};\n\nclass AudioGraphNotificationEvent : public EngineNotificationEvent\n{\npublic:\n    enum class Action\n    {\n        PROCESSOR_CREATED,\n        PROCESSOR_DELETED,\n        PROCESSOR_ADDED_TO_TRACK,\n        PROCESSOR_REMOVED_FROM_TRACK,\n        PROCESSOR_UPDATED,\n        TRACK_CREATED,\n        TRACK_DELETED\n    };\n\n    AudioGraphNotificationEvent(Action action,\n                                ObjectId processor_id,\n                                ObjectId track_id,\n                                Time timestamp) : EngineNotificationEvent(timestamp),\n                                                  _action(action),\n                                                  _processor(processor_id),\n                                                  _track(track_id) {}\n\n    [[nodiscard]] bool     is_audio_graph_notification() const override {return true;}\n    [[nodiscard]] Action   action() const {return _action;}\n    [[nodiscard]] ObjectId processor() const {return _processor;}\n    [[nodiscard]] ObjectId track() const {return _track;}\n\nprivate:\n    Action   _action;\n    ObjectId _processor;\n    ObjectId _track;\n};\n\nclass TempoNotificationEvent : public EngineNotificationEvent\n{\npublic: TempoNotificationEvent(float tempo, Time timestamp) : EngineNotificationEvent(timestamp),\n                                                              _tempo(tempo) {}\n\n    [[nodiscard]] bool is_tempo_notification() const override {return true;}\n    [[nodiscard]] float tempo() const {return _tempo;}\n\nprivate:\n    float _tempo;\n};\n\nclass TimeSignatureNotificationEvent : public EngineNotificationEvent\n{\npublic: TimeSignatureNotificationEvent(TimeSignature signature,\n                                       Time timestamp) : EngineNotificationEvent(timestamp),\n                                                         _signature(signature) {}\n\n    [[nodiscard]] bool is_time_sign_notification() const override {return true;}\n    [[nodiscard]] TimeSignature time_signature() const {return _signature;}\n\nprivate:\n    TimeSignature _signature;\n};\n\nclass PlayingModeNotificationEvent : public EngineNotificationEvent\n{\npublic: PlayingModeNotificationEvent(PlayingMode mode, Time timestamp) : EngineNotificationEvent(timestamp),\n                                                                         _mode(mode) {}\n\n    [[nodiscard]] bool is_playing_mode_notification() const override {return true;}\n    [[nodiscard]] PlayingMode mode() const {return _mode;}\n\nprivate:\n    PlayingMode _mode;\n};\n\nclass SyncModeNotificationEvent : public EngineNotificationEvent\n{\npublic: SyncModeNotificationEvent(SyncMode mode, Time timestamp) : EngineNotificationEvent(timestamp),\n                                                                   _mode(mode) {}\n\n    [[nodiscard]] bool is_sync_mode_notification() const override {return true;}\n    [[nodiscard]] SyncMode mode() const {return _mode;}\n\nprivate:\n    SyncMode _mode;\n};\n\nclass EngineTimingNotificationEvent : public EngineNotificationEvent\n{\npublic:\n    EngineTimingNotificationEvent(const performance::ProcessTimings& timings,\n                                  const std::vector<performance::ProcessTimings>& thread_timings,\n                                  Time timestamp) : EngineNotificationEvent(timestamp),\n                                                    _timings(timings),\n                                                    _thread_timings{thread_timings} {}\n\n    EngineTimingNotificationEvent(const performance::ProcessTimings& timings,\n                                  std::vector<performance::ProcessTimings>&& thread_timings,\n                                  Time timestamp) : EngineNotificationEvent(timestamp),\n                                                    _timings(timings),\n                                                    _thread_timings(std::move(thread_timings)) {}\n\n    [[nodiscard]] bool is_timing_notification() const override {return true;}\n    [[nodiscard]] const performance::ProcessTimings& main_timings() const {return _timings;}\n    [[nodiscard]] const std::vector<performance::ProcessTimings>& thread_timings() const {return _thread_timings;}\n\nprivate:\n    performance::ProcessTimings _timings;\n    std::vector<performance::ProcessTimings> _thread_timings;\n};\n\nclass EngineTimingTickNotificationEvent : public EngineNotificationEvent\n{\npublic:\n    EngineTimingTickNotificationEvent(int tick_count, Time timestamp) : EngineNotificationEvent(timestamp),\n                                                                        _tick_count(tick_count) {}\n\n    [[nodiscard]] bool is_timing_tick_notification() const override {return true;}\n    [[nodiscard]] int tick_count() const {return _tick_count;}\n\nprivate:\n    int _tick_count;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_CONTROL_EVENT_H\n"
  },
  {
    "path": "src/library/event_interface.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Abstract interface for adding event and notification functionality\n *        to a class.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_EVENT_INTERFACE_H\n#define SUSHI_EVENT_INTERFACE_H\n\n#include \"event.h\"\n\nnamespace sushi::internal {\n\nclass EventPoster\n{\npublic:\n    virtual ~EventPoster() = default;\n\n    /**\n     * @brief Function called when the poster receives an event\n     * @param event The event received\n     * @return An EventStatus or int code signaling how the event was handled.\n     *         This will be returned to the completion callback, if the event\n     *         does not have a completion callback, the return value will be\n     *         ignored\n     */\n    virtual int process(Event* /*event*/)\n    {\n        return EventStatus::UNRECOGNIZED_EVENT;\n    }\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_EVENT_INTERFACE_H\n"
  },
  {
    "path": "src/library/fixed_stack.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Simple, non-thread safe stack with a limited maximum size,\n *        implemented through an array.\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_FIXED_STACK_H\n#define SUSHI_FIXED_STACK_H\n\n#include <array>\n#include <cassert>\n\nnamespace sushi {\n\ntemplate<typename T, size_t storage_capacity>\nclass FixedStackAccessor;\n\ntemplate<typename T, size_t storage_capacity>\nclass FixedStack\n{\npublic:\n\n    /**\n     * @brief Push a new element\n     *\n     * @param element Element to be pushed\n     *\n     * @return True if success (stack not full)\n     */\n    bool push(const T& element)\n    {\n        if (full())\n        {\n            return false;\n        }\n\n        _data[++_head] = element;\n        return true;\n    }\n\n    /**\n     * @brief Pop the head of the stack\n     *\n     * @param element Output: filled with the popped head\n     *\n     * @return True if success (stack not empty)\n     */\n    bool pop(T& element)\n    {\n        if (empty())\n        {\n            return false;\n        }\n\n        element = _data[_head--];\n        return true;\n    }\n\n    /**\n     * @brief Return the current n. of elements in the stack\n     *\n     * @return Stack length\n     */\n    [[nodiscard]] int size() const\n    {\n        return (_head + 1);\n    }\n\n    /**\n     * @brief Check if stack is empty\n     */\n    [[nodiscard]] bool empty() const\n    {\n        return (_head == -1);\n    }\n\n    /**\n     * @brief Check if stack is full\n     */\n    [[nodiscard]] bool full() const\n    {\n        return (_head == (storage_capacity-1));\n    }\n\nprivate:\n    friend FixedStackAccessor<T, storage_capacity>;\n\n    std::array<T, storage_capacity> _data;\n\n    int _head {-1};\n};\n\n} // namespace sushi\n\n#endif //SUSHI_FIXED_STACK_H\n"
  },
  {
    "path": "src/library/id_generator.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Unique id generators for processors, parameters, etc.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n#ifndef SUSHI_ID_GENERATOR_H\n#define SUSHI_ID_GENERATOR_H\n\n#include <atomic>\n\ntemplate <typename T>\nclass BaseIdGenerator\n{\npublic:\n    static T new_id()\n    {\n        static std::atomic<T> counter{0};\n        return counter.fetch_add(1);\n    }\n};\n\ntypedef uint32_t ObjectId;\n\nclass ProcessorIdGenerator : public BaseIdGenerator<ObjectId>\n{ };\n\ntypedef uint16_t EventId;\n\nclass EventIdGenerator : public BaseIdGenerator<EventId>\n{ };\n#endif // SUSHI_ID_GENERATOR_H\n"
  },
  {
    "path": "src/library/internal_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Internal plugin manager.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"spdlog/fmt/bundled/format.h\"\n\n#include \"twine/twine.h\"\n\n#include \"library/internal_plugin.h\"\n\nnamespace sushi::internal {\n\nInternalPlugin::InternalPlugin(HostControl host_control) : Processor(host_control)\n{\n    _max_input_channels = DEFAULT_CHANNELS;\n    _max_output_channels = DEFAULT_CHANNELS;\n    _current_input_channels = DEFAULT_CHANNELS;\n    _current_output_channels = DEFAULT_CHANNELS;\n}\n\nFloatParameterValue* InternalPlugin::register_float_parameter(const std::string& id,\n                                                              const std::string& label,\n                                                              const std::string& unit,\n                                                              float default_value,\n                                                              float min_value,\n                                                              float max_value,\n                                                              Direction automatable,\n                                                              FloatParameterPreProcessor* pre_proc)\n{\n    if (pre_proc == nullptr)\n    {\n        pre_proc = new FloatParameterPreProcessor(min_value, max_value);\n    }\n\n    auto param = new FloatParameterDescriptor(id, label, unit, min_value, max_value, automatable, pre_proc);\n\n    if (this->register_parameter(param) == false)\n    {\n        return nullptr;\n    }\n\n    auto value = ParameterStorage::make_float_parameter_storage(param, default_value, pre_proc);\n    /* The parameter id must match the value storage index*/\n    assert(param->id() == _parameter_values.size());\n    _parameter_values.push_back(value);\n\n    return _parameter_values.back().float_parameter_value();\n}\n\nIntParameterValue* InternalPlugin::register_int_parameter(const std::string& id,\n                                                          const std::string& label,\n                                                          const std::string& unit,\n                                                          int default_value,\n                                                          int min_value,\n                                                          int max_value,\n                                                          Direction automatable,\n                                                          IntParameterPreProcessor* pre_proc)\n{\n    if (pre_proc == nullptr)\n    {\n         pre_proc = new IntParameterPreProcessor(min_value, max_value);\n    }\n\n    auto param = new IntParameterDescriptor(id, label, unit, min_value, max_value, automatable, pre_proc);\n\n    if (this->register_parameter(param) == false)\n    {\n        return nullptr;\n    }\n\n    auto value = ParameterStorage::make_int_parameter_storage(param, default_value, pre_proc);\n    /* The parameter id must match the value storage index */\n    assert(param->id() == _parameter_values.size());\n    _parameter_values.push_back(value);\n\n    return _parameter_values.back().int_parameter_value();\n}\n\nBoolParameterValue* InternalPlugin::register_bool_parameter(const std::string& id,\n                                                            const std::string& label,\n                                                            const std::string& unit,\n                                                            bool default_value,\n                                                            Direction automatable)\n{\n    auto param = new BoolParameterDescriptor(id, label, unit, true, false, automatable, nullptr);\n\n    if (!this->register_parameter(param))\n    {\n        return nullptr;\n    }\n\n    ParameterStorage value_storage = ParameterStorage::make_bool_parameter_storage(param, default_value);\n    /* The parameter id must match the value storage index */\n    assert(param->id() == _parameter_values.size());\n    _parameter_values.push_back(value_storage);\n\n    return _parameter_values.back().bool_parameter_value();\n}\n\n\nbool InternalPlugin::register_property(const std::string& name,\n                                       const std::string& label,\n                                       const std::string& default_value)\n{\n    auto param = new StringPropertyDescriptor(name, label, \"\");\n\n    if (this->register_parameter(param) == false)\n    {\n        return false;\n    }\n\n    // Push a dummy container here for ids to match\n    ParameterStorage value_storage = ParameterStorage::make_bool_parameter_storage(param, false);\n    _parameter_values.push_back(value_storage);\n    // The property value is stored here\n    _property_values[param->id()] = default_value;\n    return true;\n}\n\nvoid InternalPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n        case RtEventType::NOTE_ON:\n        case RtEventType::NOTE_OFF:\n        case RtEventType::NOTE_AFTERTOUCH:\n        case RtEventType::PITCH_BEND:\n        case RtEventType::AFTERTOUCH:\n        case RtEventType::MODULATION:\n        case RtEventType::WRAPPED_MIDI_EVENT:\n        {\n            /* The default behaviour is to pass keyboard events through unchanged */\n            output_event(event);\n            break;\n        }\n\n        case RtEventType::FLOAT_PARAMETER_CHANGE:\n        case RtEventType::INT_PARAMETER_CHANGE:\n        case RtEventType::BOOL_PARAMETER_CHANGE:\n        {\n            /* These are \"managed events\" where this function provides a default\n             * implementation for handling these and setting parameter values */\n            _handle_parameter_event(event.parameter_change_event());\n            break;\n        }\n\n        case RtEventType::SET_STATE:\n        {\n            auto state = event.processor_state_event()->state();\n            _set_rt_state(state);\n            async_delete(state);\n            break;\n        }\n\n        case RtEventType::STRING_PROPERTY_CHANGE:\n        {\n            /* In order to handle STRING_PROPERTY_CHANGE events in the rt_thread, override\n             * process_event() and handle it. Then call this base function which will automatically\n             * schedule a delete event that will be executed in the non-rt domain */\n            auto typed_event = event.property_change_event();\n            async_delete(typed_event->deletable_value());\n            break;\n        }\n\n        default:\n            break;\n    }\n}\n\nvoid InternalPlugin::set_parameter_and_notify(FloatParameterValue* storage, float new_value)\n{\n    storage->set(new_value);\n\n    if (maybe_output_cv_value(storage->descriptor()->id(), new_value) == false)\n    {\n        auto e = RtEvent::make_parameter_change_event(this->id(), 0, storage->descriptor()->id(),\n                                                      storage->normalized_value());\n        output_event(e);\n    }\n}\n\nvoid InternalPlugin::set_parameter_and_notify(IntParameterValue* storage, int new_value)\n{\n    storage->set(static_cast<float>(new_value));\n    auto e = RtEvent::make_parameter_change_event(this->id(), 0, storage->descriptor()->id(), storage->normalized_value());\n    output_event(e);\n}\n\nvoid InternalPlugin::set_parameter_and_notify(BoolParameterValue* storage, bool new_value)\n{\n    storage->set(new_value);\n    auto e = RtEvent::make_parameter_change_event(this->id(), 0, storage->descriptor()->id(), storage->normalized_value());\n    output_event(e);\n}\n\nstd::pair<ProcessorReturnCode, float> InternalPlugin::parameter_value(ObjectId parameter_id) const\n{\n    if (parameter_id >= _parameter_values.size())\n    {\n        return {ProcessorReturnCode::PARAMETER_NOT_FOUND, 0.0f};\n    }\n\n    const auto& value_storage = _parameter_values[parameter_id];\n\n    if (value_storage.type() == ParameterType::FLOAT)\n    {\n        float norm_value = value_storage.float_parameter_value()->normalized_value();\n        return {ProcessorReturnCode::OK, norm_value};\n    }\n    else if (value_storage.type() == ParameterType::INT)\n    {\n        float norm_value = value_storage.int_parameter_value()->normalized_value();\n        return {ProcessorReturnCode::OK, norm_value};\n    }\n    else if (value_storage.type() == ParameterType::BOOL)\n    {\n        return {ProcessorReturnCode::OK, value_storage.bool_parameter_value()->domain_value() ? 1.0f : 0.0f};\n    }\n\n    return {ProcessorReturnCode::PARAMETER_ERROR, 0.0f};\n}\n\nstd::pair<ProcessorReturnCode, float> InternalPlugin::parameter_value_in_domain(ObjectId parameter_id) const\n{\n    if (parameter_id >= _parameter_values.size())\n    {\n        return {ProcessorReturnCode::PARAMETER_NOT_FOUND, 0.0f};\n    }\n\n    const auto& value_storage = _parameter_values[parameter_id];\n\n    if (value_storage.type() == ParameterType::FLOAT)\n    {\n        return {ProcessorReturnCode::OK, value_storage.float_parameter_value()->domain_value()};\n    }\n    else if (value_storage.type() == ParameterType::INT)\n    {\n        return {ProcessorReturnCode::OK, static_cast<float>(value_storage.int_parameter_value()->domain_value())};\n    }\n    else if (value_storage.type() == ParameterType::BOOL)\n    {\n        return {ProcessorReturnCode::OK, value_storage.bool_parameter_value()->domain_value() ? 1.0f : 0.0f};\n    }\n\n    return {ProcessorReturnCode::PARAMETER_ERROR, 0.0f};\n}\n\nstd::pair<ProcessorReturnCode, std::string> InternalPlugin::parameter_value_formatted(ObjectId parameter_id) const\n{\n    if (parameter_id >= _parameter_values.size())\n    {\n        return {ProcessorReturnCode::PARAMETER_NOT_FOUND, \"\"};\n    }\n\n    const auto& value_storage = _parameter_values[parameter_id];\n\n    if (value_storage.type() == ParameterType::FLOAT)\n    {\n        return {ProcessorReturnCode::OK, fmt::format(\"{0:0.2f}\", value_storage.float_parameter_value()->domain_value())};\n    }\n    else if (value_storage.type() == ParameterType::INT)\n    {\n        return {ProcessorReturnCode::OK, std::to_string(value_storage.int_parameter_value()->domain_value())};\n    }\n    else if (value_storage.type() == ParameterType::BOOL)\n    {\n        return {ProcessorReturnCode::OK, value_storage.bool_parameter_value()->processed_value() ? \"True\" : \"False\"};\n    }\n\n    return {ProcessorReturnCode::PARAMETER_ERROR, \"\"};\n}\n\nstd::pair<ProcessorReturnCode, std::string> InternalPlugin::property_value(ObjectId property_id) const\n{\n    std::scoped_lock<std::mutex> lock(_property_lock);\n    auto node = _property_values.find(property_id);\n    if (node == _property_values.end())\n    {\n        return {ProcessorReturnCode::PARAMETER_NOT_FOUND, \"\"};\n    }\n    return {ProcessorReturnCode::OK, node->second};\n}\n\nProcessorReturnCode InternalPlugin::set_property_value(ObjectId property_id, const std::string& value)\n{\n    std::scoped_lock<std::mutex> lock(_property_lock);\n    auto node = _property_values.find(property_id);\n    if (node == _property_values.end())\n    {\n        return ProcessorReturnCode::PARAMETER_NOT_FOUND;\n    }\n    node->second = value;\n    _host_control.post_event(std::make_unique<PropertyChangeNotificationEvent>(this->id(),\n                                                                               property_id,\n                                                                               value,\n                                                                               IMMEDIATE_PROCESS));\n    return ProcessorReturnCode::OK;\n}\n\nProcessorReturnCode InternalPlugin::set_state(ProcessorState* state, bool realtime_running)\n{\n    for (const auto& property : state->properties())\n    {\n        this->set_property_value(property.first, property.second);\n    }\n\n    if (realtime_running)\n    {\n        auto rt_state = std::make_unique<RtState>(*state);\n        _host_control.post_event(std::make_unique<RtStateEvent>(this->id(), std::move(rt_state), IMMEDIATE_PROCESS));\n    }\n    else\n    {\n        if (state->bypassed().has_value())\n        {\n            this->set_bypassed(state->bypassed().value());\n        }\n        for (const auto& parameter : state->parameters())\n        {\n            auto event = RtEvent::make_parameter_change_event(this->id(), 0, parameter.first, parameter.second);\n            this->process_event(event);\n        }\n        _host_control.post_event(std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_UPDATED,\n                                                                               this->id(), 0, IMMEDIATE_PROCESS));\n    }\n    return ProcessorReturnCode::OK;\n}\n\nProcessorState InternalPlugin::save_state() const\n{\n    ProcessorState state;\n    state.set_bypass(this->bypassed());\n    for (const auto& property : this->_property_values)\n    {\n        state.add_property_change(property.first, property.second);\n    }\n    for (const auto& parameter : _parameter_values)\n    {\n        switch (parameter.type())\n        {\n            case ParameterType::BOOL:\n                state.add_parameter_change(parameter.id(), parameter.bool_parameter_value()->normalized_value());\n                break;\n\n            case ParameterType::INT:\n                state.add_parameter_change(parameter.id(), parameter.int_parameter_value()->normalized_value());\n                break;\n\n            case ParameterType::FLOAT:\n                state.add_parameter_change(parameter.id(), parameter.float_parameter_value()->normalized_value());\n                break;\n\n            default:\n                break;\n        }\n    }\n    return state;\n}\n\nPluginInfo InternalPlugin::info() const\n{\n    PluginInfo info;\n    info.type = PluginType::INTERNAL;\n    info.path = \"\";\n    info.uid = this->uid();\n    return info;\n}\n\nvoid InternalPlugin::send_data_to_realtime(BlobData data, int id)\n{\n    assert(twine::is_current_thread_realtime() == false);\n    _host_control.post_event(std::make_unique<DataPropertyEvent>(this->id(), id, data, IMMEDIATE_PROCESS));\n}\n\nvoid InternalPlugin::send_property_to_realtime(ObjectId property_id, const std::string& value)\n{\n    assert(twine::is_current_thread_realtime() == false);\n    _host_control.post_event(std::make_unique<StringPropertyEvent>(this->id(), property_id, value, IMMEDIATE_PROCESS));\n}\n\nvoid InternalPlugin::_set_rt_state(const RtState* state)\n{\n    if (state->bypassed().has_value())\n    {\n        this->set_bypassed(*state->bypassed());\n    }\n    for (const auto& parameter : state->parameters())\n    {\n        auto event = RtEvent::make_parameter_change_event(this->id(), 0, parameter.first, parameter.second);\n        this->process_event(event);\n    }\n    notify_state_change_rt();\n}\n\nvoid InternalPlugin::_handle_parameter_event(const ParameterChangeRtEvent* event)\n{\n    if (event->param_id() < _parameter_values.size())\n    {\n        auto storage = &_parameter_values[event->param_id()];\n\n        switch (storage->type())\n        {\n            case ParameterType::FLOAT:\n            {\n                auto parameter_value = storage->float_parameter_value();\n                if (parameter_value->descriptor()->automatable())\n                {\n                    parameter_value->set(event->value());\n                }\n                break;\n            }\n            case ParameterType::INT:\n            {\n                auto parameter_value = storage->int_parameter_value();\n                if (parameter_value->descriptor()->automatable())\n                {\n                    parameter_value->set(event->value());\n                }\n                break;\n            }\n            case ParameterType::BOOL:\n            {\n                auto parameter_value = storage->bool_parameter_value();\n                if (parameter_value->descriptor()->automatable())\n                {\n                    parameter_value->set(event->value());\n                }\n                break;\n            }\n            default:\n                break;\n        }\n    }\n}\n\n} // end namespace sushi::internal\n"
  },
  {
    "path": "src/library/internal_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Internal plugin base class.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_INTERNAL_PLUGIN_H\n#define SUSHI_INTERNAL_PLUGIN_H\n\n#include <deque>\n#include <unordered_map>\n#include <mutex>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n#include \"library/processor.h\"\n#include \"library/plugin_parameters.h\"\n\nnamespace sushi::internal {\n\nconstexpr int DEFAULT_CHANNELS = MAX_TRACK_CHANNELS;\n\nclass StringUid\n{\npublic:\n    virtual ~StringUid() = default;\n\n    virtual std::string_view uid() const\n    {\n        return \"\";\n    }\n};\n\n/**\n * @brief CRTP helper to avoid having to implement both a static function and\n *        a virtual one to access the plugin string uid\n *\n *        Usage: Implement static_uid() and inherit from UidHelper<ClassName>\n */\ntemplate <typename T>\nclass UidHelper : public virtual StringUid\n{\npublic:\n    ~UidHelper() override = default;\n\n    virtual std::string_view uid() const override\n    {\n        return T::static_uid();\n    }\n};\n\nclass InternalPluginAccessor;\n\n/**\n * @brief internal base class for processors that keeps track of all host-related\n * configuration and provides basic parameter and event handling.\n */\nclass InternalPlugin : public Processor, public virtual StringUid\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(InternalPlugin)\n\n    explicit InternalPlugin(HostControl host_control);\n\n    ~InternalPlugin() override = default;\n\n    void process_event(const RtEvent& event) override;\n\n    std::pair<ProcessorReturnCode, float> parameter_value(ObjectId parameter_id) const override;\n\n    std::pair<ProcessorReturnCode, float> parameter_value_in_domain(ObjectId parameter_id) const override;\n\n    std::pair<ProcessorReturnCode, std::string> parameter_value_formatted(ObjectId parameter_id) const override;\n\n    std::pair<ProcessorReturnCode, std::string> property_value(ObjectId property_id) const override;\n\n    ProcessorReturnCode set_property_value(ObjectId property_id, const std::string& value) override;\n\n    ProcessorReturnCode set_state(ProcessorState* state, bool realtime_running) override;\n\n    ProcessorState save_state() const override;\n\n    /**\n     * @brief Register a float typed parameter and return a pointer to a value\n     *        storage object that will hold the value and set automatically when\n     *        the processor receives parameter change events\n     * @param name The unique name of the parameter\n     * @param label The display name of the parameter\n     * @param unit The unit of the parameters display value\n     * @param default_value The default value the parameter should have\n     * @param min_value The minimum value the parameter can have\n     * @param max_value The maximum value the parameter can have\n     * @param automatable Whether the parameter can be automated, or if not, that it then is output-only\n     * @param pre_proc An optional preprocessor object used to clip/scale the set value\n     * @return Pointer to a FloatParameterValue object\n     */\n    FloatParameterValue* register_float_parameter(const std::string& name,\n                                                  const std::string& label,\n                                                  const std::string& unit,\n                                                  float default_value,\n                                                  float min_value,\n                                                  float max_value,\n                                                  Direction automatable,\n                                                  FloatParameterPreProcessor* pre_proc = nullptr);\n\n    /**\n     * @brief Register an int typed parameter and return a pointer to a value\n     *        storage object that will hold the value and set automatically when\n     *        the processor receives parameter change events\n     * @param name The unique name of the parameter\n     * @param label The display name of the parameter\n     * @param unit The unit of the parameters display value\n     * @param default_value The default value the parameter should have\n     * @param min_value The minimum value the parameter can have\n     * @param max_value The maximum value the parameter can have\n     * @param automatable Whether the parameter can be automated, or if not, that it then is output-only\n     * @param pre_proc An optional preprocessor object used to clip/scale the set value\n     * @return Pointer to an IntParameterValue object\n     */\n    IntParameterValue* register_int_parameter(const std::string& name,\n                                              const std::string& label,\n                                              const std::string& unit,\n                                              int default_value,\n                                              int min_value,\n                                              int max_value,\n                                              Direction automatable,\n                                              IntParameterPreProcessor* pre_proc = nullptr);\n\n    /**\n     * @brief Register a bool typed parameter and return a pointer to a value\n     *        storage object that will hold the value and set automatically when\n     *        the processor receives parameter change events\n     * @param name The unique name of the parameter\n     * @param label The display name of the parameter\n     * @param unit The unit of the parameters display value\n     * @param default_value The default value the parameter should have\n     * @param automatable Whether the parameter can be automated, or if not, that it then is output-only\n     * @return Pointer to a BoolParameterValue object\n     */\n    BoolParameterValue* register_bool_parameter(const std::string& name,\n                                                const std::string& label,\n                                                const std::string& unit,\n                                                bool default_value,\n                                                Direction automatable);\n\n    /**\n     * @brief Register a string property that can be updated through events. String\n     *        properties will be updated in the non-rt thread. For string parameters\n     *        to be received in an rt thread, call send_property_to_realtime()\n     *        when received.\n     * @param name Unique name of the property\n     * @param label Display name of the property\n     * @param default_value The default value of the property\n     * @return true if the property was registered successfully\n     */\n    bool register_property(const std::string& name,\n                           const std::string& label,\n                           const std::string& default_value);\n\n    PluginInfo info() const override;\n\nprotected:\n    /**\n     * @brief Update the value of a parameter and send an event notifying\n     *        the host of the change.\n     * @param storage The ParameterValue to update\n     * @param new_value The new value to use\n     */\n    void set_parameter_and_notify(FloatParameterValue* storage, float new_value);\n\n    /**\n     * @brief Update the value of a parameter and send an event notifying\n     *        the host of the change.\n     * @param storage The ParameterValue to update\n     * @param new_value The new value to use\n     */\n    void set_parameter_and_notify(IntParameterValue* storage, int new_value);\n\n    /**\n     * @brief Update the value of a parameter and send an event notifying\n     *        the host of the change.\n     * @param storage The ParameterValue to update\n     * @param new_value The new value to use\n     */\n    void set_parameter_and_notify(BoolParameterValue* storage, bool new_value);\n\n    /**\n     * @brief Pass opaque data to the realtime part of the plugin in a thread-safe manner\n     *        The data will be passed as an RtEvent with type DATA_PROPERTY_CHANGE.\n     * @param data The data to pass, memory management is the responsibility of the receiver.\n     * @param id An identifier that will be used to populate the property_id field_of the RtEvent.\n     */\n    void send_data_to_realtime(BlobData data, int id);\n\n    /**\n     * @brief Pass a string property value to the realtime part of the plugin in a thread-\n     *        safe manner and with managed lifetime. The string will be passed to the rt-\n     *        thread as an RtEvent with type STRING_PROPERTY_CHANGE.\n     * @param property_id The id of the string property whose value is changed.\n     * @param value The string value to pass. Lifetime will be handled automatically.\n     */\n    void send_property_to_realtime(ObjectId property_id, const std::string& value);\n\nprivate:\n    friend InternalPluginAccessor;\n\n    void _set_rt_state(const RtState* state);\n\n    void _handle_parameter_event(const ParameterChangeRtEvent* event);\n\n    /* TODO: Consider container type to use here. Deque has the very desirable property\n     *  that iterators are never invalidated by adding to the containers.\n     *  For arrays or std::vectors we need to know the maximum capacity for that to work. */\n    std::deque<ParameterStorage> _parameter_values;\n\n    mutable std::mutex _property_lock;\n    std::unordered_map<ObjectId, std::string> _property_values;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_INTERNAL_PLUGIN_H\n"
  },
  {
    "path": "src/library/internal_processor_factory.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include \"internal_processor_factory.h\"\n\n#include \"plugins/passthrough_plugin.h\"\n#include \"plugins/gain_plugin.h\"\n#include \"plugins/lfo_plugin.h\"\n#include \"plugins/equalizer_plugin.h\"\n#include \"plugins/arpeggiator_plugin.h\"\n#include \"plugins/sample_player_plugin.h\"\n#include \"plugins/peak_meter_plugin.h\"\n#include \"plugins/transposer_plugin.h\"\n#include \"plugins/step_sequencer_plugin.h\"\n#include \"plugins/cv_to_control_plugin.h\"\n#include \"plugins/control_to_cv_plugin.h\"\n#include \"plugins/wav_writer_plugin.h\"\n#include \"plugins/wav_streamer_plugin.h\"\n#include \"plugins/mono_summing_plugin.h\"\n#include \"plugins/send_return_factory.h\"\n#include \"plugins/sample_delay_plugin.h\"\n#include \"plugins/stereo_mixer_plugin.h\"\n#include \"plugins/freeverb_plugin.h\"\n\n#include \"plugins/brickworks/compressor_plugin.h\"\n#include \"plugins/brickworks/cab_sim_plugin.h\"\n#include \"plugins/brickworks/ring_mod_plugin.h\"\n#include \"plugins/brickworks/bitcrusher_plugin.h\"\n#include \"plugins/brickworks/wah_plugin.h\"\n#include \"plugins/brickworks/eq3band_plugin.h\"\n#include \"plugins/brickworks/phaser_plugin.h\"\n#include \"plugins/brickworks/chorus_plugin.h\"\n#include \"plugins/brickworks/vibrato_plugin.h\"\n#include \"plugins/brickworks/flanger_plugin.h\"\n#include \"plugins/brickworks/combdelay_plugin.h\"\n#include \"plugins/brickworks/saturation_plugin.h\"\n#include \"plugins/brickworks/noise_gate_plugin.h\"\n#include \"plugins/brickworks/tremolo_plugin.h\"\n#include \"plugins/brickworks/notch_plugin.h\"\n#include \"plugins/brickworks/multi_filter_plugin.h\"\n#include \"plugins/brickworks/highpass_plugin.h\"\n#include \"plugins/brickworks/clip_plugin.h\"\n#include \"plugins/brickworks/fuzz_plugin.h\"\n#include \"plugins/brickworks/dist_plugin.h\"\n#include \"plugins/brickworks/drive_plugin.h\"\n#include \"plugins/brickworks/combdelay_plugin.h\"\n#include \"plugins/brickworks/notch_plugin.h\"\n#include \"plugins/brickworks/simple_synth_plugin.h\"\n\nnamespace sushi::internal {\n\n/**\n * @brief Simplified factory only used internally in this file\n */\nclass BaseInternalPlugFactory\n{\npublic:\n    virtual ~BaseInternalPlugFactory() = default;\n    virtual std::string_view uid() const = 0;\n    virtual std::shared_ptr<Processor> create(const HostControl &host_control) = 0;\n};\n\ntemplate<class T>\nclass InternalFactory : public BaseInternalPlugFactory\n{\npublic:\n    std::string_view uid() const override\n    {\n        return T::static_uid();\n    }\n\n    std::shared_ptr<Processor> create(const HostControl &host_control) override\n    {\n        return std::make_shared<T>(host_control);\n    }\n};\n\nInternalProcessorFactory::InternalProcessorFactory() : _send_return_factory(std::make_unique<SendReturnFactory>())\n{\n    /* When adding new internal plugins, make sure they implement static_uid()\n     * then add a line here to add them to the factory  */\n    _add(std::make_unique<InternalFactory<passthrough_plugin::PassthroughPlugin>>());\n    _add(std::make_unique<InternalFactory<gain_plugin::GainPlugin>>());\n    _add(std::make_unique<InternalFactory<lfo_plugin::LfoPlugin>>());\n    _add(std::make_unique<InternalFactory<equalizer_plugin::EqualizerPlugin>>());\n    _add(std::make_unique<InternalFactory<sample_player_plugin::SamplePlayerPlugin>>());\n    _add(std::make_unique<InternalFactory<arpeggiator_plugin::ArpeggiatorPlugin>>());\n    _add(std::make_unique<InternalFactory<peak_meter_plugin::PeakMeterPlugin>>());\n    _add(std::make_unique<InternalFactory<transposer_plugin::TransposerPlugin>>());\n    _add(std::make_unique<InternalFactory<step_sequencer_plugin::StepSequencerPlugin>>());\n    _add(std::make_unique<InternalFactory<cv_to_control_plugin::CvToControlPlugin>>());\n    _add(std::make_unique<InternalFactory<control_to_cv_plugin::ControlToCvPlugin>>());\n    _add(std::make_unique<InternalFactory<wav_writer_plugin::WavWriterPlugin>>());\n    _add(std::make_unique<InternalFactory<wav_streamer_plugin::WavStreamerPlugin>>());\n    _add(std::make_unique<InternalFactory<mono_summing_plugin::MonoSummingPlugin>>());\n    _add(std::make_unique<InternalFactory<sample_delay_plugin::SampleDelayPlugin>>());\n    _add(std::make_unique<InternalFactory<stereo_mixer_plugin::StereoMixerPlugin>>());\n    _add(std::make_unique<InternalFactory<freeverb_plugin::FreeverbPlugin>>());\n    _add(std::make_unique<InternalFactory<compressor_plugin::CompressorPlugin>>());\n    _add(std::make_unique<InternalFactory<cab_sim_plugin::CabSimPlugin>>());\n    _add(std::make_unique<InternalFactory<ring_mod_plugin::RingModPlugin>>());\n    _add(std::make_unique<InternalFactory<bitcrusher_plugin::BitcrusherPlugin>>());\n    _add(std::make_unique<InternalFactory<wah_plugin::WahPlugin>>());\n    _add(std::make_unique<InternalFactory<eq3band_plugin::Eq3bandPlugin>>());\n    _add(std::make_unique<InternalFactory<phaser_plugin::PhaserPlugin>>());\n    _add(std::make_unique<InternalFactory<chorus_plugin::ChorusPlugin>>());\n    _add(std::make_unique<InternalFactory<vibrato_plugin::VibratoPlugin>>());\n    _add(std::make_unique<InternalFactory<flanger_plugin::FlangerPlugin>>());\n    _add(std::make_unique<InternalFactory<comb_plugin::CombPlugin>>());\n    _add(std::make_unique<InternalFactory<saturation_plugin::SaturationPlugin>>());\n    _add(std::make_unique<InternalFactory<noise_gate_plugin::NoiseGatePlugin>>());\n    _add(std::make_unique<InternalFactory<tremolo_plugin::TremoloPlugin>>());\n    _add(std::make_unique<InternalFactory<notch_plugin::NotchPlugin>>());\n    _add(std::make_unique<InternalFactory<multi_filter_plugin::MultiFilterPlugin>>());\n    _add(std::make_unique<InternalFactory<highpass_plugin::HighPassPlugin>>());\n    _add(std::make_unique<InternalFactory<clip_plugin::ClipPlugin>>());\n    _add(std::make_unique<InternalFactory<fuzz_plugin::FuzzPlugin>>());\n    _add(std::make_unique<InternalFactory<dist_plugin::DistPlugin>>());\n    _add(std::make_unique<InternalFactory<drive_plugin::DrivePlugin>>());\n    _add(std::make_unique<InternalFactory<simple_synth_plugin::SimpleSynthPlugin>>());\n}\n\nstd::pair<ProcessorReturnCode, std::shared_ptr<Processor>>\nInternalProcessorFactory::new_instance(const PluginInfo &plugin_info,\n                                       HostControl &host_control,\n                                       float sample_rate)\n{\n    if (plugin_info.uid == send_plugin::SendPlugin::static_uid() ||\n        plugin_info.uid == return_plugin::ReturnPlugin::static_uid())\n    {\n        return _send_return_factory->new_instance(plugin_info, host_control, sample_rate);\n    }\n\n    auto processor = _create_internal_plugin(plugin_info.uid, host_control);\n    if (processor == nullptr)\n    {\n        return {ProcessorReturnCode::ERROR, nullptr};\n    }\n    else\n    {\n        auto processor_status = processor->init(sample_rate);\n        return {processor_status, processor};\n    }\n}\n\nInternalProcessorFactory::~InternalProcessorFactory() = default;\n\nstd::shared_ptr<Processor> InternalProcessorFactory::_create_internal_plugin(const std::string &uid,\n                                                                             HostControl &host_control)\n{\n    auto factory = _internal_plugin_factories.find(uid);\n    if (factory == _internal_plugin_factories.end())\n    {\n        return nullptr;\n    }\n    return factory->second->create(host_control);\n}\n\nvoid InternalProcessorFactory::_add(std::unique_ptr<BaseInternalPlugFactory> factory)\n{\n    _internal_plugin_factories.insert({factory->uid(), std::move(factory)});\n}\n\n} // end namespace sushi::internal\n"
  },
  {
    "path": "src/library/internal_processor_factory.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Factory for Internal processors.\n */\n\n#ifndef SUSHI_INTERNAL_PROCESSOR_FACTORY_H\n#define SUSHI_INTERNAL_PROCESSOR_FACTORY_H\n\n#include \"library/base_processor_factory.h\"\n\nnamespace sushi::internal {\n\nclass BaseProcessorFactory;\nclass BaseInternalPlugFactory;\n\nclass InternalProcessorFactory : public BaseProcessorFactory\n{\npublic:\n    InternalProcessorFactory();\n\n    ~InternalProcessorFactory() override;\n\n    std::pair<ProcessorReturnCode, std::shared_ptr<Processor>> new_instance(const PluginInfo &plugin_info,\n                                                                            HostControl& host_control,\n                                                                            float sample_rate) override;\nprivate:\n    /**\n     * @brief Instantiate a plugin instance of a given type\n     * @param uid String unique id\n     * @param host_control\n     * @return Pointer to plugin instance if uid is valid, nullptr otherwise\n     */\n    std::shared_ptr<Processor> _create_internal_plugin(const std::string& uid, HostControl& host_control);\n\n    void _add(std::unique_ptr<BaseInternalPlugFactory> factory);\n\n    std::unique_ptr<BaseProcessorFactory> _send_return_factory;\n\n    std::unordered_map<std::string_view, std::unique_ptr<BaseInternalPlugFactory>> _internal_plugin_factories;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_INTERNAL_PROCESSOR_FACTORY_H\n"
  },
  {
    "path": "src/library/lv2/lv2_control.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"lv2_control.h\"\n\nnamespace sushi::internal::lv2 {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"lv2\");\n\nControlID ControlID::new_port_control(Port* port, Model* model, uint32_t index)\n{\n    const auto lilvPort = port->lilv_port();\n    const auto plugin = model->plugin_class();\n    const auto nodes = model->nodes();\n\n    ControlID id;\n    id.model = model;\n    id.type = ControlType::PORT;\n    id.node = lilv_node_duplicate(lilv_port_get_node(plugin, lilvPort));\n    id.symbol = lilv_node_duplicate(lilv_port_get_symbol(plugin, lilvPort));\n    id.label = lilv_port_get_name(plugin, lilvPort);\n    id.index = index;\n    id.group = lilv_port_get(plugin, lilvPort, nodes->pg_group);\n    id.value_type = model->forge().Float;\n    id.is_writable = lilv_port_is_a(plugin, lilvPort, nodes->lv2_InputPort);\n    id.is_readable = lilv_port_is_a(plugin, lilvPort, nodes->lv2_OutputPort);\n    id.is_toggle = lilv_port_has_property(plugin, lilvPort, nodes->lv2_toggled);\n    id.is_integer = lilv_port_has_property(plugin, lilvPort, nodes->lv2_integer);\n    id.is_enumeration = lilv_port_has_property(plugin, lilvPort, nodes->lv2_enumeration);\n    id.is_logarithmic = lilv_port_has_property(plugin, lilvPort, nodes->pprops_logarithmic);\n\n    lilv_port_get_range(plugin, lilvPort, &id.def, &id.min, &id.max);\n    if (lilv_port_has_property(plugin, lilvPort, nodes->lv2_sampleRate))\n    {\n        /* Adjust range for lv2:sampleRate controls */\n        if (lilv_node_is_float(id.min) || lilv_node_is_int(id.min))\n        {\n            const float min = lilv_node_as_float(id.min) * model->sample_rate();\n            lilv_node_free(id.min);\n            id.min = lilv_new_float(model->lilv_world(), min);\n        }\n        if (lilv_node_is_float(id.max) || lilv_node_is_int(id.max))\n        {\n            const float max = lilv_node_as_float(id.max) * model->sample_rate();\n            lilv_node_free(id.max);\n            id.max = lilv_new_float(model->lilv_world(), max);\n        }\n    }\n\n    /* Get scale points */\n    auto scale_points = lilv_port_get_scale_points(plugin, lilvPort);\n    if (scale_points)\n    {\n        LILV_FOREACH(scale_points, s, scale_points)\n        {\n            const auto scale_point = lilv_scale_points_get(scale_points, s);\n\n            if (lilv_node_is_float(lilv_scale_point_get_value(scale_point)) ||\n                lilv_node_is_int(lilv_scale_point_get_value(scale_point)))\n            {\n                auto sp = ScalePoint();\n\n                sp.value = lilv_node_as_float(lilv_scale_point_get_value(scale_point));\n                sp.label = lilv_node_as_string(lilv_scale_point_get_label(scale_point));\n                id.scale_points.push_back(std::move(sp));\n            }\n        }\n\n        std::sort(id.scale_points.begin(), id.scale_points.end(),\n      [](const ScalePoint& a, const ScalePoint& b)\n            {\n                return a.value < b.value;\n            }\n         );\n\n        lilv_scale_points_free(scale_points);\n    }\n\n    return id;\n}\n\nbool ControlID::has_range(Model* model, const LilvNode* subject, const char* range_uri)\n{\n    auto world = model->lilv_world();\n    auto range = lilv_new_uri(world, range_uri);\n    const bool result = lilv_world_ask(world, subject, model->nodes()->rdfs_range, range);\n    lilv_node_free(range);\n    return result;\n}\n\nControlID ControlID::new_property_control(Model* model, const LilvNode* property)\n{\n    auto world = model->lilv_world();\n\n    ControlID id;\n    id.model = model;\n    id.type = ControlType::PROPERTY;\n    id.node = lilv_node_duplicate(property);\n    id.symbol = lilv_world_get_symbol(world, property);\n    id.label = lilv_world_get(world, property, model->nodes()->rdfs_label, nullptr);\n    id.property = model->get_map().map(model, lilv_node_as_uri(property));\n\n    id.min = lilv_world_get(world, property, model->nodes()->lv2_minimum, nullptr);\n    id.max = lilv_world_get(world, property, model->nodes()->lv2_maximum, nullptr);\n    id.def = lilv_world_get(world, property, model->nodes()->lv2_default, nullptr);\n\n    const char *const types[] = {\n            LV2_ATOM__Int, LV2_ATOM__Long, LV2_ATOM__Float, LV2_ATOM__Double,\n            LV2_ATOM__Bool, LV2_ATOM__String, LV2_ATOM__Path, nullptr\n    };\n\n    for (const char *const *t = types; *t; ++t)\n    {\n        if (ControlID::has_range(model, property, *t))\n        {\n            id.value_type = model->get_map().map(model, *t);\n            break;\n        }\n    }\n\n    auto forge = model->forge();\n\n    id.is_toggle = (id.value_type == forge.Bool);\n    id.is_integer = (id.value_type == forge.Int ||\n                      id.value_type == forge.Long);\n\n    if (id.value_type == false)\n    {\n        ELKLOG_LOG_ERROR(\"Unknown value type for property {}\", lilv_node_as_string(property));\n    }\n\n    return id;\n}\n\n} // end namespace sushi::internal::lv2\n"
  },
  {
    "path": "src/library/lv2/lv2_control.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief LV2 plugin control class - internally used, for holding the data of LV2 plugin controls.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_LV2_CONTROL_H\n#define SUSHI_LV2_CONTROL_H\n\n#ifdef SUSHI_BUILD_WITH_LV2\n\n#include \"lv2_model.h\"\n\nnamespace sushi::internal::lv2 {\n\nstruct ScalePoint\n{\n    float value;\n    std::string label;\n};\n\n/** Type of plugin control. */\nenum class ControlType\n{\n    PORT, // Control port\n    PROPERTY // Property (set via atom message)\n};\n\n/** Plugin control. */\nstruct ControlID\n{\npublic:\n    ~ControlID() = default;\n\n    static ControlID new_port_control(Port* port, Model* model, uint32_t index);\n    static ControlID new_property_control(Model* model, const LilvNode* property);\n\n    static bool has_range(Model* model, const LilvNode* subject, const char* range_uri);\n\n    Model* model {nullptr};\n    ControlType type{ControlType::PORT};\n    LilvNode* node {nullptr};\n    LilvNode* symbol {nullptr};\n    LilvNode* label {nullptr};\n    LV2_URID property {0}; // Iff type == PROPERTY\n    int index {0}; // Iff type == PORT\n    LilvNode* group {nullptr}; // Port/control group, or NULL\n\n    std::vector<ScalePoint> scale_points;\n    LV2_URID value_type;\n    LilvNode* min;\n    LilvNode* max;\n    LilvNode* def;\n\n    bool is_toggle {false};\n    bool is_integer {false};\n    bool is_enumeration {false};\n    bool is_logarithmic {false};\n    bool is_writable {false}; // Writable (input)\n    bool is_readable {false}; // Readable (output)\n\nprivate:\n    explicit ControlID() = default;\n};\n\n} // end namespace sushi::internal::lv2\n\n#endif // SUSHI_BUILD_WITH_LV2\n\n#endif // SUSHI_LV2_CONTROL_H\n"
  },
  {
    "path": "src/library/lv2/lv2_features.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Wrapper for LV2 plugins - Wrapper for LV2 plugins - extra features.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifdef SUSHI_BUILD_WITH_LV2\n\n#include <twine/twine.h>\n\n#include \"elklog/static_logger.h\"\n\n#include \"lv2_features.h\"\n\nnamespace sushi::internal::lv2 {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"lv2\");\n\nPort* port_by_symbol(Model* model, const char* sym)\n{\n    for (int i = 0; i < model->port_count(); ++i)\n    {\n        auto port = model->get_port(i);\n        const auto port_symbol = lilv_port_get_symbol(model->plugin_class(), port->lilv_port());\n\n        if (strcmp(lilv_node_as_string(port_symbol), sym) == 0)\n        {\n            return port;\n        }\n    }\n\n    return nullptr;\n}\n\nint lv2_log(LV2_Log_Handle handle,\n            LV2_URID type,\n            const char* msg,\n            va_list args)\n{\n    auto model = static_cast<Model*>(handle);\n    auto urids = model->urids();\n\n    // Convert from old style printf-formatting\n    std::array<char, 1024> msg_buffer{\"\"};\n    if (vsnprintf(msg_buffer.data(), msg_buffer.size(), msg, args) > 0)\n    {\n        if (type == urids.log_Trace && TRACE_OPTION)\n        {\n            ELKLOG_LOG_DEBUG(\"LV2 Trace: {}\", msg_buffer.data());\n        }\n        else if (type == urids.log_Error)\n        {\n            ELKLOG_LOG_DEBUG(\"LV2 Error: {}\", msg_buffer.data());\n        }\n        else if (type == urids.log_Warning)\n        {\n            ELKLOG_LOG_DEBUG(\"LV2 Warning: {}\", msg_buffer.data());\n        }\n        else if (type == urids.log_Entry)\n        {\n            ELKLOG_LOG_DEBUG(\"LV2 Entry: {}\", msg_buffer.data());\n        }\n        else if (type == urids.log_Note)\n        {\n            ELKLOG_LOG_DEBUG(\"LV2 Note: {}\", msg_buffer.data());\n        }\n        else if (type == urids.log_log)\n        {\n            ELKLOG_LOG_DEBUG(\"LV2 Log: {}\", msg_buffer.data());\n        }\n        else\n        {\n            ELKLOG_LOG_DEBUG(\"LV2 unknown error: {}\", msg_buffer.data());\n        }\n    }\n\n    return 0;\n}\n\nint lv2_vprintf(LV2_Log_Handle handle,\n                LV2_URID type,\n                [[maybe_unused]] const char* fmt,\n                va_list args)\n{\n    if (twine::is_current_thread_realtime())\n    {\n        // Logging from a realtime thread is not yet supported\n        return 0;\n    }\n\n    lv2_log(handle, type, fmt, args);\n    return 0;\n}\n\nint lv2_printf(LV2_Log_Handle handle,\n               LV2_URID type,\n               const char *fmt, ...)\n{\n    if (twine::is_current_thread_realtime())\n    {\n        // Logging from a realtime thread is not yet supported\n        return 0;\n    }\n\n    va_list args;\n    va_start(args, fmt);\n    lv2_log(handle, type, fmt, args);\n    va_end(args);\n    return 0;\n}\n\nchar* make_path(LV2_State_Make_Path_Handle handle, const char* path)\n{\n    auto model = static_cast<Model*>(handle);\n\n    // Create in save directory if saving, otherwise use temp directory\n\n    std::string made_path;\n\n    if (model->save_dir().empty() == false)\n    {\n        made_path = model->save_dir() + path;\n    }\n    else\n    {\n        made_path = model->temp_dir() + path;\n    }\n\n    const std::string::size_type size = made_path.size();\n    char* buffer = new char[size + 1]; // Extra char for null\n    memcpy(buffer, made_path.c_str(), size + 1);\n    return buffer;\n}\n\nLV2_URID map_uri(LV2_URID_Map_Handle handle, const char* uri)\n{\n    auto model = static_cast<Model*>(handle);\n    return model->map(uri);\n}\n\nconst char* unmap_uri(LV2_URID_Unmap_Handle handle, LV2_URID urid)\n{\n    auto model = static_cast<Model*>(handle);\n    return model->unmap(urid);\n}\n\nvoid init_feature(LV2_Feature* const dest, const char* const URI, void* data)\n{\n    dest->URI = URI;\n    dest->data = data;\n}\n\n} // end namespace sushi::internal::lv2\n\n#endif // SUSHI_BUILD_WITH_LV2\n"
  },
  {
    "path": "src/library/lv2/lv2_features.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Callbacks for LV2 extension features.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_LV2_FEATURES_H\n#define SUSHI_LV2_FEATURES_H\n\n#ifdef SUSHI_BUILD_WITH_LV2\n\n#include <map>\n#include <mutex>\n\n#include <lilv-0/lilv/lilv.h>\n\n#include <lv2/resize-port/resize-port.h>\n#include <lv2/midi/midi.h>\n#include <lv2/log/log.h>\n#include <lv2/atom/atom.h>\n#include <lv2/atom/forge.h>\n#include <lv2/buf-size/buf-size.h>\n#include <lv2/data-access/data-access.h>\n#include <lv2/options/options.h>\n#include <lv2/parameters/parameters.h>\n#include <lv2/patch/patch.h>\n#include <lv2/port-groups/port-groups.h>\n#include <lv2/port-props/port-props.h>\n#include <lv2/presets/presets.h>\n#include <lv2/state/state.h>\n#include <lv2/time/time.h>\n#include <lv2/ui/ui.h>\n#include <lv2/urid/urid.h>\n#include <lv2/worker/worker.h>\n\n#include \"library/processor.h\"\n#include \"engine/base_event_dispatcher.h\"\n\n#include \"lv2_model.h\"\n\nnamespace sushi::internal::lv2 {\n\n// writing also LV2 Trace Log messages to file.\n// This is set to false by default as it will cause thousands of mode switches.\nstatic constexpr bool TRACE_OPTION = false;\n\n/**\n   Get a port structure by symbol.\n*/\nPort* port_by_symbol(Model* model, const char* sym);\n\n// These two are callbacks for the LV2 logging macro.\nint lv2_vprintf(LV2_Log_Handle handle,\n            LV2_URID type,\n            const char *fmt,\n            va_list args);\n\nint lv2_printf(LV2_Log_Handle handle,\n           LV2_URID type,\n           const char *fmt, ...);\n\ntypedef int (*PresetSink)(Model* model,\n                          const LilvNode* node,\n                          const LilvNode* title,\n                          void* data);\n\nchar* make_path(LV2_State_Make_Path_Handle handle, const char* path);\n\nLV2_URID map_uri(LV2_URID_Map_Handle handle, const char* uri);\n\nconst char* unmap_uri(LV2_URID_Unmap_Handle handle, LV2_URID urid);\n\nvoid init_feature(LV2_Feature* const dest, const char* const URI, void* data);\n\n} // end namespace sushi::internal::lv2\n\n#endif // SUSHI_BUILD_WITH_LV2\n\n#endif // SUSHI_LV2_FEATURES_H\n"
  },
  {
    "path": "src/library/lv2/lv2_host_nodes.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief LV2 plugin host nodes. Internally used class for each plugin instance Nodes list.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_LV2_HOST_NODES_H\n#define SUSHI_LV2_HOST_NODES_H\n\n#ifdef SUSHI_BUILD_WITH_LV2\n\n#include <lilv-0/lilv/lilv.h>\n\n#include <lv2/atom/atom.h>\n#include <lv2/resize-port/resize-port.h>\n#include <lv2/port-props/port-props.h>\n#include <lv2/port-groups/port-groups.h>\n#include <lv2/presets/presets.h>\n#include <lv2/state/state.h>\n#include <lv2/urid/urid.h>\n#include <lv2/worker/worker.h>\n#include <lv2/midi/midi.h>\n\nnamespace sushi::internal::lv2 {\n\nclass HostNodes\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(HostNodes);\n\n    explicit HostNodes(LilvWorld* world)\n    {\n        /* Cache URIs for concepts we'll use */\n        this->atom_AtomPort = lilv_new_uri(world, LV2_ATOM__AtomPort);\n        this->atom_Chunk = lilv_new_uri(world, LV2_ATOM__Chunk);\n        this->atom_Float = lilv_new_uri(world, LV2_ATOM__Float);\n        this->atom_Path = lilv_new_uri(world, LV2_ATOM__Path);\n        this->atom_Sequence = lilv_new_uri(world, LV2_ATOM__Sequence);\n        this->lv2_AudioPort = lilv_new_uri(world, LV2_CORE__AudioPort);\n        this->lv2_CVPort = lilv_new_uri(world, LV2_CORE__CVPort);\n        this->lv2_ControlPort = lilv_new_uri(world, LV2_CORE__ControlPort);\n        this->lv2_InputPort = lilv_new_uri(world, LV2_CORE__InputPort);\n        this->lv2_OutputPort = lilv_new_uri(world, LV2_CORE__OutputPort);\n        this->lv2_connectionOptional = lilv_new_uri(world, LV2_CORE__connectionOptional);\n        this->lv2_control = lilv_new_uri(world, LV2_CORE__control);\n        this->lv2_default = lilv_new_uri(world, LV2_CORE__default);\n        this->lv2_enumeration = lilv_new_uri(world, LV2_CORE__enumeration);\n        this->lv2_integer = lilv_new_uri(world, LV2_CORE__integer);\n        this->lv2_maximum = lilv_new_uri(world, LV2_CORE__maximum);\n        this->lv2_minimum = lilv_new_uri(world, LV2_CORE__minimum);\n        this->lv2_name = lilv_new_uri(world, LV2_CORE__name);\n        this->lv2_reportsLatency = lilv_new_uri(world, LV2_CORE__reportsLatency);\n        this->lv2_sampleRate = lilv_new_uri(world, LV2_CORE__sampleRate);\n        this->lv2_symbol = lilv_new_uri(world, LV2_CORE__symbol);\n        this->lv2_toggled = lilv_new_uri(world, LV2_CORE__toggled);\n        this->midi_MidiEvent = lilv_new_uri(world, LV2_MIDI__MidiEvent);\n        this->pg_group = lilv_new_uri(world, LV2_PORT_GROUPS__group);\n        this->pprops_logarithmic = lilv_new_uri(world, LV2_PORT_PROPS__logarithmic);\n        this->pprops_notOnGUI = lilv_new_uri(world, LV2_PORT_PROPS__notOnGUI);\n        this->pprops_rangeSteps = lilv_new_uri(world, LV2_PORT_PROPS__rangeSteps);\n        this->pset_Preset = lilv_new_uri(world, LV2_PRESETS__Preset);\n        this->pset_bank = lilv_new_uri(world, LV2_PRESETS__bank);\n        this->rdfs_comment = lilv_new_uri(world, LILV_NS_RDFS \"comment\");\n        this->rdfs_label = lilv_new_uri(world, LILV_NS_RDFS \"label\");\n        this->rdfs_range = lilv_new_uri(world, LILV_NS_RDFS \"range\");\n        this->rsz_minimumSize = lilv_new_uri(world, LV2_RESIZE_PORT__minimumSize);\n\n        this->work_interface = lilv_new_uri(world, LV2_WORKER__interface);\n        this->work_schedule = lilv_new_uri(world, LV2_WORKER__schedule);\n    }\n\n    ~HostNodes()\n    {\n        lilv_node_free(atom_AtomPort);\n        lilv_node_free(atom_Chunk);\n        lilv_node_free(atom_Float);\n        lilv_node_free(atom_Path);\n        lilv_node_free(atom_Sequence);\n\n        lilv_node_free(lv2_AudioPort);\n        lilv_node_free(lv2_CVPort);\n        lilv_node_free(lv2_ControlPort);\n        lilv_node_free(lv2_InputPort);\n        lilv_node_free(lv2_OutputPort);\n        lilv_node_free(lv2_connectionOptional);\n        lilv_node_free(lv2_control);\n        lilv_node_free(lv2_default);\n        lilv_node_free(lv2_enumeration);\n        lilv_node_free(lv2_integer);\n        lilv_node_free(lv2_maximum);\n        lilv_node_free(lv2_minimum);\n        lilv_node_free(lv2_name);\n        lilv_node_free(lv2_reportsLatency);\n        lilv_node_free(lv2_sampleRate);\n        lilv_node_free(lv2_symbol);\n        lilv_node_free(lv2_toggled);\n        lilv_node_free(midi_MidiEvent);\n        lilv_node_free(pg_group);\n        lilv_node_free(pprops_logarithmic);\n        lilv_node_free(pprops_notOnGUI);\n        lilv_node_free(pprops_rangeSteps);\n        lilv_node_free(pset_Preset);\n        lilv_node_free(pset_bank);\n        lilv_node_free(rdfs_comment);\n        lilv_node_free(rdfs_label);\n        lilv_node_free(rdfs_range);\n        lilv_node_free(rsz_minimumSize);\n\n        lilv_node_free(work_interface);\n        lilv_node_free(work_schedule);\n    }\n\n    LilvNode* atom_AtomPort;\n    LilvNode* atom_Chunk;\n    LilvNode* atom_Float;\n    LilvNode* atom_Path;\n    LilvNode* atom_Sequence;\n\n    LilvNode* lv2_AudioPort;\n    LilvNode* lv2_CVPort;\n    LilvNode* lv2_ControlPort;\n    LilvNode* lv2_InputPort;\n    LilvNode* lv2_OutputPort;\n    LilvNode* lv2_connectionOptional;\n    LilvNode* lv2_control;\n    LilvNode* lv2_default;\n    LilvNode* lv2_enumeration;\n    LilvNode* lv2_integer;\n    LilvNode* lv2_maximum;\n    LilvNode* lv2_minimum;\n    LilvNode* lv2_name;\n    LilvNode* lv2_reportsLatency;\n    LilvNode* lv2_sampleRate;\n    LilvNode* lv2_symbol;\n    LilvNode* lv2_toggled;\n    LilvNode* midi_MidiEvent;\n    LilvNode* pg_group;\n    LilvNode* pprops_logarithmic;\n    LilvNode* pprops_notOnGUI;\n    LilvNode* pprops_rangeSteps;\n    LilvNode* pset_Preset;\n    LilvNode* pset_bank;\n    LilvNode* rdfs_comment;\n    LilvNode* rdfs_label;\n    LilvNode* rdfs_range;\n    LilvNode* rsz_minimumSize;\n\n    LilvNode* work_interface;\n    LilvNode* work_schedule;\n};\n\n#endif // SUSHI_BUILD_WITH_LV2\n\n} // end namespace sushi::internal::lv2\n\n#endif // SUSHI_LV2_HOST_NODES_H\n"
  },
  {
    "path": "src/library/lv2/lv2_model.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Wrapper for LV2 plugins - Wrapper for LV2 plugins - model.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifdef SUSHI_BUILD_WITH_LV2\n\n#include \"elklog/static_logger.h\"\n\n#include \"lv2_model.h\"\n#include \"lv2_features.h\"\n#include \"lv2_worker.h\"\n#include \"lv2_wrapper.h\"\n#include \"lv2_state.h\"\n\n\nnamespace {\n/** These features have no data */\nconst LV2_Feature static_features[] = {\n        { LV2_STATE__loadDefaultState, nullptr },\n        { LV2_BUF_SIZE__powerOf2BlockLength, nullptr },\n        { LV2_BUF_SIZE__fixedBlockLength, nullptr },\n        { LV2_BUF_SIZE__boundedBlockLength, nullptr } };\n\n} // anonymous namespace\n\nnamespace sushi::internal::lv2 {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"lv2\");\n\nModel::Model(float sample_rate, LV2_Wrapper* wrapper, LilvWorld* world): _sample_rate(sample_rate),\n                                                                         _wrapper(wrapper),\n                                                                         _world(world)\n{\n    _nodes = std::make_unique<HostNodes>(_world);\n\n    // This allows loading plug-ins from their URI's, assuming they are installed in the correct paths\n    // on the local machine.\n\n    _initialize_map_feature();\n    _initialize_unmap_feature();\n    _initialize_urid_symap();\n    _initialize_log_feature();\n    _initialize_make_path_feature();\n\n    _initialize_worker_feature();\n    _initialize_safe_restore_feature();\n    _initialize_options_feature();\n}\n\nModel::~Model()\n{\n    if (_plugin_instance)\n    {\n        lilv_instance_deactivate(_plugin_instance);\n        lilv_instance_free(_plugin_instance);\n\n        for (unsigned i = 0; i < _controls.size(); ++i)\n        {\n            auto control = _controls[i];\n            lilv_node_free(control.node);\n            lilv_node_free(control.symbol);\n            lilv_node_free(control.label);\n\n            // This can optionally be null for some plugins.\n            if (control.group != nullptr)\n            {\n                lilv_node_free(control.group);\n            }\n\n            lilv_node_free(control.min);\n            lilv_node_free(control.max);\n            lilv_node_free(control.def);\n        }\n\n        _plugin_instance = nullptr;\n\n        _lv2_state->unload_programs();\n    }\n\n    // Explicitly setting to nullptr, so that destructor is invoked before world is freed.\n    _nodes = nullptr;\n\n    symap_free(_symap);\n}\n\nvoid Model::_initialize_host_feature_list()\n{\n    // Build feature list for passing to plugins.\n    // Warning: LV2 / Lilv require this list to be null-terminated.\n    // So remember to check for null when iterating over it!\n    std::array<const LV2_Feature*, FEATURE_LIST_SIZE> features({\n            &_features.map_feature,\n            &_features.unmap_feature,\n            &_features.log_feature,\n            &_features.sched_feature,\n            &_features.make_path_feature,\n            &_features.options_feature,\n            &static_features[0],\n            &static_features[1],\n            &static_features[2],\n            &static_features[3],\n            nullptr\n    });\n\n    _feature_list = features;\n}\n\nbool Model::_feature_is_supported(const std::string& uri)\n{\n    if (uri.compare(\"http://lv2plug.in/ns/lv2core#isLive\") == 0)\n    {\n        return true;\n    }\n\n    for (const auto f : _feature_list)\n    {\n        if (f == nullptr) // The last element is by LV2 required to be null.\n            break;\n\n        if (uri.compare(f->URI) == 0)\n        {\n            return true;\n        }\n    }\n\n    return false;\n}\n\nProcessorReturnCode Model::load_plugin(const LilvPlugin* plugin_handle, double sample_rate)\n{\n    _plugin_class = plugin_handle;\n    _play_state = PlayState::PAUSED;\n    _sample_rate = sample_rate;\n\n    _initialize_host_feature_list();\n\n    if (std::getenv(\"LV2_PATH\") == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"The LV2_PATH environment variable is not set on your system \"\n                        \"- this is required for loading LV2 plugins.\");\n        return ProcessorReturnCode::PLUGIN_INIT_ERROR;\n    }\n\n    if (_check_for_required_features(plugin_handle) == false)\n    {\n        return ProcessorReturnCode::PLUGIN_INIT_ERROR;\n    }\n\n    _plugin_instance = lilv_plugin_instantiate(plugin_handle, sample_rate, _feature_list.data());\n\n    if (_plugin_instance == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to load LV2 - Plugin entry point not found.\");\n        return ProcessorReturnCode::PLUGIN_INIT_ERROR;\n    }\n\n    _features.ext_data.data_access = lilv_instance_get_descriptor(_plugin_instance)->extension_data;\n\n    /* Create workers if necessary */\n    if (lilv_plugin_has_extension_data(plugin_handle, _nodes->work_interface))\n    {\n        const void* iface_raw = lilv_instance_get_extension_data(_plugin_instance, LV2_WORKER__interface);\n        auto iface = static_cast<const LV2_Worker_Interface*>(iface_raw);\n\n        _worker->init(iface, true);\n        if (_safe_restore)\n        {\n            _state_worker->init(iface, false);\n        }\n    }\n\n    auto state_threadSafeRestore = lilv_new_uri(_world, LV2_STATE__threadSafeRestore);\n    if (lilv_plugin_has_feature(plugin_handle, state_threadSafeRestore))\n    {\n        _safe_restore = true;\n    }\n    lilv_node_free(state_threadSafeRestore);\n\n    if (_create_ports(plugin_handle) == false)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to allocate ports for LV2 plugin.\");\n        return ProcessorReturnCode::PLUGIN_INIT_ERROR;\n    }\n\n    _lv2_state = std::make_unique<State>(this);\n\n    _lv2_state->populate_program_list();\n\n    auto state = lilv_state_new_from_world(_world,\n                                           &get_map(),\n                                           lilv_plugin_get_uri(plugin_handle));\n\n    if (state != nullptr) // Apply loaded state to plugin instance if necessary\n    {\n        _lv2_state->apply_state(state, false);\n        lilv_state_free(state);\n    }\n\n    _create_controls(true);\n    _create_controls(false);\n\n    return ProcessorReturnCode::OK;\n}\n\nbool Model::_create_ports(const LilvPlugin* plugin)\n{\n    _input_audio_channel_count = 0;\n    _output_audio_channel_count = 0;\n\n    const int port_count = lilv_plugin_get_num_ports(plugin);\n\n    std::vector<float> default_values(port_count);\n\n    lilv_plugin_get_port_ranges_float(plugin, nullptr, nullptr, default_values.data());\n\n    for (int i = 0; i < port_count; ++i)\n    {\n        auto new_port = _create_port(plugin, i, default_values[i]);\n\n        // If it fails to create a new port, loading has failed, and the host shuts down.\n        if(new_port.flow() != PortFlow::FLOW_INPUT &&\n           new_port.flow() != PortFlow::FLOW_OUTPUT &&\n           new_port.optional() == false)\n        {\n            ELKLOG_LOG_ERROR(\"Mandatory LV2 port has unknown type (neither input nor output)\");\n            return false;\n        }\n\n        // If the port has an unknown data type, loading has failed, and the host shuts down.\n        if( new_port.type() != PortType::TYPE_CONTROL &&\n            new_port.type() != PortType::TYPE_AUDIO &&\n            new_port.type() != PortType::TYPE_EVENT &&\n            new_port.optional() == false)\n        {\n            ELKLOG_LOG_ERROR(\"Mandatory LV2 port has unknown data type.\");\n            return false;\n        }\n\n        add_port(new_port);\n    }\n\n    const auto control_input = lilv_plugin_get_port_by_designation(plugin,\n                                                                   nodes()->lv2_InputPort,\n                                                                   nodes()->lv2_control);\n\n    // The (optional) lv2:designation of this port is lv2:control,\n    // which indicates that this is the \"main\" control port where the host should send events\n    // it expects to configure the plugin, for example changing the MIDI program.\n    // This is necessary since it is possible to have several MIDI input ports,\n    // though typically it is best to have one.\n    if (control_input != nullptr)\n    {\n        set_control_input_index(lilv_port_get_index(plugin, control_input));\n    }\n\n    return true;\n}\n\n/**\nCreate a port from data description. This is called before plugin\nand Jack instantiation. The remaining instance-specific setup\n(e.g. buffers) is done later in activate_port().\n\nException Port::FailedCreation can be thrown in the port constructor!\n*/\nPort Model::_create_port(const LilvPlugin* plugin, int port_index, float default_value)\n{\n    Port port(plugin, port_index, default_value, this);\n\n    if (port.type() == PortType::TYPE_AUDIO)\n    {\n        if (port.flow() == PortFlow::FLOW_INPUT)\n        {\n            _input_audio_channel_count++;\n        }\n        else if (port.flow() == PortFlow::FLOW_OUTPUT)\n        {\n            _output_audio_channel_count++;\n        }\n    }\n\n    return port;\n}\n\nvoid Model::_create_controls(bool writable)\n{\n    const auto uri_node = lilv_plugin_get_uri(_plugin_class);\n    auto patch_writable = lilv_new_uri(_world, LV2_PATCH__writable);\n    auto patch_readable = lilv_new_uri(_world, LV2_PATCH__readable);\n    const std::string uri_as_string = lilv_node_as_string(uri_node);\n\n    auto properties = lilv_world_find_nodes(\n            _world,\n            uri_node,\n            writable ? patch_writable : patch_readable,\n            nullptr);\n\n    LILV_FOREACH(nodes, p, properties)\n    {\n        const auto property = lilv_nodes_get(properties, p);\n\n        bool found = false;\n        if ((writable == false) && lilv_world_ask(_world,\n                                                  uri_node,\n                                                  patch_writable,\n                                                  property))\n        {\n            // Find existing writable control\n            for (auto& control : _controls)\n            {\n                if (lilv_node_equals(control.node, property))\n                {\n                    found = true;\n                    control.is_readable = true;\n                    break;\n                }\n            }\n\n            if (found)\n            {\n                continue; // This skips subsequent.\n            }\n        }\n\n        auto record = ControlID::new_property_control(this, property);\n\n        if (writable)\n        {\n            record.is_writable = true;\n        }\n        else\n        {\n            record.is_readable = true;\n        }\n\n        if (record.value_type)\n        {\n            _controls.push_back(std::move(record));\n        }\n        else\n        {\n            ELKLOG_LOG_ERROR(\"Parameter {} has unknown value type, ignored\", lilv_node_as_string(record.node));\n        }\n    }\n\n    lilv_nodes_free(properties);\n\n    lilv_node_free(patch_readable);\n    lilv_node_free(patch_writable);\n}\n\nvoid Model::_initialize_urid_symap()\n{\n    lv2_atom_forge_init(&this->_forge, &_map);\n\n    this->_urids.atom_Float = symap_map(this->_symap, LV2_ATOM__Float);\n    this->_urids.atom_Int = symap_map(this->_symap, LV2_ATOM__Int);\n    this->_urids.atom_Object = symap_map(this->_symap, LV2_ATOM__Object);\n    this->_urids.atom_Path = symap_map(this->_symap, LV2_ATOM__Path);\n    this->_urids.atom_String = symap_map(this->_symap, LV2_ATOM__String);\n    this->_urids.atom_eventTransfer = symap_map(this->_symap, LV2_ATOM__eventTransfer);\n    this->_urids.bufsz_maxBlockLength = symap_map(this->_symap, LV2_BUF_SIZE__maxBlockLength);\n    this->_urids.bufsz_minBlockLength = symap_map(this->_symap, LV2_BUF_SIZE__minBlockLength);\n    this->_urids.bufsz_sequenceSize = symap_map(this->_symap, LV2_BUF_SIZE__sequenceSize);\n    this->_urids.log_Error = symap_map(this->_symap, LV2_LOG__Error);\n    this->_urids.log_Trace = symap_map(this->_symap, LV2_LOG__Trace);\n    this->_urids.log_Warning = symap_map(this->_symap, LV2_LOG__Warning);\n    this->_urids.log_Entry = symap_map(this->_symap, LV2_LOG__Entry);\n    this->_urids.log_Note = symap_map(this->_symap, LV2_LOG__Note);\n    this->_urids.log_log = symap_map(this->_symap, LV2_LOG__log);\n    this->_urids.midi_MidiEvent = symap_map(this->_symap, LV2_MIDI__MidiEvent);\n    this->_urids.param_sampleRate = symap_map(this->_symap, LV2_PARAMETERS__sampleRate);\n    this->_urids.patch_Get = symap_map(this->_symap, LV2_PATCH__Get);\n    this->_urids.patch_Put = symap_map(this->_symap, LV2_PATCH__Put);\n    this->_urids.patch_Set = symap_map(this->_symap, LV2_PATCH__Set);\n    this->_urids.patch_body = symap_map(this->_symap, LV2_PATCH__body);\n    this->_urids.patch_property = symap_map(this->_symap, LV2_PATCH__property);\n    this->_urids.patch_value = symap_map(this->_symap, LV2_PATCH__value);\n    this->_urids.time_Position = symap_map(this->_symap, LV2_TIME__Position);\n    this->_urids.time_bar = symap_map(this->_symap, LV2_TIME__bar);\n    this->_urids.time_barBeat = symap_map(this->_symap, LV2_TIME__barBeat);\n    this->_urids.time_beatUnit = symap_map(this->_symap, LV2_TIME__beatUnit);\n    this->_urids.time_beatsPerBar = symap_map(this->_symap, LV2_TIME__beatsPerBar);\n    this->_urids.time_beatsPerMinute = symap_map(this->_symap, LV2_TIME__beatsPerMinute);\n    this->_urids.time_frame = symap_map(this->_symap, LV2_TIME__frame);\n    this->_urids.time_speed = symap_map(this->_symap, LV2_TIME__speed);\n    this->_urids.ui_updateRate = symap_map(this->_symap, LV2_UI__updateRate);\n}\n\nvoid Model::_initialize_log_feature()\n{\n    this->_features.llog.handle = this;\n    this->_features.llog.printf = lv2_printf;\n    this->_features.llog.vprintf = lv2_vprintf;\n    init_feature(&_features.log_feature, LV2_LOG__log, &_features.llog);\n}\n\nvoid Model::_initialize_map_feature()\n{\n    this->_symap = lv2_host::symap_new();\n    this->_map.handle = this;\n    this->_map.map = map_uri;\n    init_feature(&this->_features.map_feature, LV2_URID__map, &this->_map);\n\n    lv2_atom_forge_init(&_forge, &_map);\n}\n\nvoid Model::_initialize_unmap_feature()\n{\n    this->_unmap.handle = this;\n    this->_unmap.unmap = unmap_uri;\n    init_feature(&this->_features.unmap_feature, LV2_URID__unmap, &this->_unmap);\n}\n\nState* Model::state()\n{\n    return _lv2_state.get();\n}\n\nvoid Model::_initialize_make_path_feature()\n{\n    this->_features.make_path.handle = this;\n    this->_features.make_path.path = make_path;\n    init_feature(&this->_features.make_path_feature,\n                 LV2_STATE__makePath, &this->_features.make_path);\n}\n\nvoid Model::_initialize_worker_feature()\n{\n    _worker = std::make_unique<Worker>(this);\n\n    _features.sched.handle = _worker.get();\n    _features.sched.schedule_work = Worker::schedule;\n    init_feature(&_features.sched_feature,\n                 LV2_WORKER__schedule, &_features.sched);\n}\n\nvoid Model::_initialize_safe_restore_feature()\n{\n    _state_worker = std::make_unique<Worker>(this);\n\n    _features.ssched.handle = _state_worker.get();\n    _features.ssched.schedule_work = Worker::schedule;\n    init_feature(&_features.state_sched_feature,\n                 LV2_WORKER__schedule, &_features.ssched);\n\n    init_feature(&this->_features.safe_restore_feature,\n                 LV2_STATE__threadSafeRestore,\n                 nullptr);\n}\n\nvoid Model::_initialize_options_feature()\n{\n    /* Build options array to pass to plugin */\n    const LV2_Options_Option options[6] = {\n            { LV2_OPTIONS_INSTANCE, 0, _urids.param_sampleRate, sizeof(float), _urids.atom_Float, &_sample_rate },\n            { LV2_OPTIONS_INSTANCE, 0, _urids.bufsz_minBlockLength, sizeof(int32_t), _urids.atom_Int, &_buffer_size },\n            { LV2_OPTIONS_INSTANCE, 0, _urids.bufsz_maxBlockLength, sizeof(int32_t), _urids.atom_Int, &_buffer_size },\n            { LV2_OPTIONS_INSTANCE, 0, _urids.bufsz_sequenceSize, sizeof(int32_t), _urids.atom_Int, &_midi_buffer_size },\n            { LV2_OPTIONS_INSTANCE, 0, _urids.ui_updateRate, sizeof(float), _urids.atom_Float, &_ui_update_hz },\n            { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, nullptr }\n    };\n\n    std::copy(std::begin(options), std::end(options), std::begin(_features.options));\n\n    init_feature(&_features.options_feature,\n                 LV2_OPTIONS__options,\n                 (void*)_features.options.data());\n}\n\nbool Model::_check_for_required_features(const LilvPlugin* plugin)\n{\n    // Check that any required features are supported\n    auto required_features = lilv_plugin_get_required_features(plugin);\n\n    LILV_FOREACH(nodes, f, required_features)\n    {\n        auto node = lilv_nodes_get(required_features, f);\n        auto uri = lilv_node_as_uri(node);\n\n        if (_feature_is_supported(uri) == false)\n        {\n            ELKLOG_LOG_ERROR(\"LV2 feature {} is not supported.\", uri);\n\n            return false;\n        }\n    }\n\n    lilv_nodes_free(required_features);\n    return true;\n}\n\nstd::array<const LV2_Feature*, FEATURE_LIST_SIZE>* Model::host_feature_list()\n{\n    return &_feature_list;\n}\n\nLilvWorld* Model::lilv_world()\n{\n    return _world;\n}\n\nLilvInstance* Model::plugin_instance()\n{\n    return _plugin_instance;\n}\n\nconst LilvPlugin* Model::plugin_class()\n{\n    return _plugin_class;\n}\n\nint Model::midi_buffer_size() const\n{\n    return _midi_buffer_size;\n}\n\nfloat Model::sample_rate() const\n{\n    return _sample_rate;\n}\n\nPort* Model::get_port(int index)\n{\n    return &_ports[index];\n}\n\nvoid Model::add_port(Port port)\n{\n    _ports.push_back(port);\n}\n\nint Model::port_count()\n{\n    return _ports.size();\n}\n\nconst HostNodes* Model::nodes()\n{\n    return _nodes.get();\n}\n\nconst LV2_URIDs& Model::urids()\n{\n    return _urids;\n}\n\nLV2_URID_Map& Model::get_map()\n{\n    return _map;\n}\n\nLV2_URID_Unmap& Model::get_unmap()\n{\n    return _unmap;\n}\n\nLV2_URID Model::map(const char* uri)\n{\n    std::unique_lock<std::mutex> lock(_symap_lock);\n    return symap_map(_symap, uri);\n}\n\nconst char* Model::unmap(LV2_URID urid)\n{\n    std::unique_lock<std::mutex> lock(_symap_lock);\n    const char* uri = symap_unmap(_symap, urid);\n\n    return uri;\n}\n\nLV2_Atom_Forge& Model::forge()\n{\n    return _forge;\n}\n\nint Model::plugin_latency() const\n{\n    return _plugin_latency;\n}\n\nvoid Model::set_plugin_latency(int latency)\n{\n    _plugin_latency = latency;\n}\n\nvoid Model::set_control_input_index(int index)\n{\n    _control_input_index = index;\n}\n\nbool Model::update_requested() const\n{\n    return _request_update;\n}\n\nvoid Model::request_update()\n{\n    _request_update = true;\n}\n\nvoid Model::clear_update_request()\n{\n    _request_update = false;\n}\n\nvoid Model::set_play_state(PlayState play_state)\n{\n    _play_state = play_state;\n}\n\nPlayState Model::play_state() const\n{\n    return _play_state;\n}\n\nstd::string Model::temp_dir()\n{\n    return _temp_dir;\n}\n\nstd::string Model::save_dir()\n{\n    return _save_dir;\n}\n\nvoid Model::set_save_dir(const std::string& save_dir)\n{\n    _save_dir = save_dir;\n}\n\nbool Model::buf_size_set() const\n{\n    return _buf_size_set;\n}\n\nstd::vector<ControlID>& Model::controls()\n{\n    return _controls;\n}\n\nuint32_t Model::position() const\n{\n    return _position;\n}\n\nvoid Model::set_position(uint32_t position)\n{\n    _position = position;\n}\n\nfloat Model::bpm() const\n{\n    return _bpm;\n}\n\nvoid Model::set_bpm(float bpm)\n{\n    _bpm = bpm;\n}\n\nbool Model::rolling() const\n{\n    return _rolling;\n}\n\nvoid Model::set_rolling(bool rolling)\n{\n    _rolling = rolling;\n}\n\nstd::pair<LilvState*, bool> Model::state_to_set()\n{\n    return {_state_to_set, _delete_state_after_use};\n}\n\nvoid Model::set_state_to_set(LilvState* state_to_set, bool delete_after_use)\n{\n    _state_to_set = state_to_set;\n    _delete_state_after_use = delete_after_use;\n}\n\nint Model::input_audio_channel_count() const\n{\n    return _input_audio_channel_count;\n}\n\nint Model::output_audio_channel_count() const\n{\n    return _output_audio_channel_count;\n}\n\nWorker* Model::worker()\n{\n    return _worker.get();\n}\n\nWorker* Model::state_worker()\n{\n    return _state_worker.get();\n}\n\nbool Model::safe_restore()\n{\n    return _safe_restore;\n}\n\nLV2_Wrapper* Model::wrapper()\n{\n    return _wrapper;\n}\n\n} // end namespace sushi::internal::lv2\n\n#endif // SUSHI_BUILD_WITH_LV2\n"
  },
  {
    "path": "src/library/lv2/lv2_model.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief LV2 plugin host Model - initializes and holds the LV2 model for each plugin instance.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_LV2_MODEL_H\n#define SUSHI_LV2_MODEL_H\n\n#ifdef SUSHI_BUILD_WITH_LV2\n\n#include <map>\n#include <mutex>\n\n#include <lv2/log/log.h>\n#include <lv2/options/options.h>\n#include <lv2/data-access/data-access.h>\n#include <lv2/atom/forge.h>\n\n#include \"lv2_host/lv2_symap.h\"\n\n#include \"library/processor.h\"\n#include \"engine/base_event_dispatcher.h\"\n\n#include \"lv2_port.h\"\n#include \"lv2_host_nodes.h\"\n#include \"lv2_control.h\"\n\nnamespace sushi::internal::lv2 {\n\nclass Model;\nclass State;\nclass Worker;\nstruct ControlID;\nclass LV2_Wrapper;\n\n/**\nControl change event, sent through ring buffers for UI updates.\n*/\nstruct ControlChange\n{\n    uint32_t index;\n    uint32_t protocol;\n    uint32_t size;\n    uint8_t body[];\n};\n\nstruct LV2_URIDs\n{\n    LV2_URID atom_Float;\n    LV2_URID atom_Int;\n    LV2_URID atom_Object;\n    LV2_URID atom_Path;\n    LV2_URID atom_String;\n    LV2_URID atom_eventTransfer;\n    LV2_URID bufsz_maxBlockLength;\n    LV2_URID bufsz_minBlockLength;\n    LV2_URID bufsz_sequenceSize;\n    LV2_URID log_Error;\n    LV2_URID log_Trace;\n    LV2_URID log_Warning;\n    LV2_URID log_Entry;\n    LV2_URID log_Note;\n    LV2_URID log_log;\n    LV2_URID midi_MidiEvent;\n    LV2_URID param_sampleRate;\n    LV2_URID patch_Get;\n    LV2_URID patch_Put;\n    LV2_URID patch_Set;\n    LV2_URID patch_body;\n    LV2_URID patch_property;\n    LV2_URID patch_value;\n    LV2_URID time_Position;\n    LV2_URID time_bar;\n    LV2_URID time_barBeat;\n    LV2_URID time_beatUnit;\n    LV2_URID time_beatsPerBar;\n    LV2_URID time_beatsPerMinute;\n    LV2_URID time_frame;\n    LV2_URID time_speed;\n    LV2_URID ui_updateRate;\n};\n\nenum class PlayState\n{\n    RUNNING,\n    PAUSE_REQUESTED,\n    PAUSED\n};\n\nstruct HostFeatures\n{\n    LV2_Feature map_feature;\n    LV2_Feature unmap_feature;\n    LV2_State_Make_Path make_path;\n    LV2_Feature make_path_feature;\n\n    LV2_Worker_Schedule sched;\n    LV2_Feature sched_feature;\n    LV2_Worker_Schedule ssched;\n    LV2_Feature state_sched_feature;\n    LV2_Feature safe_restore_feature;\n\n    LV2_Log_Log llog;\n    LV2_Feature log_feature;\n    std::array<LV2_Options_Option, 6> options;\n    LV2_Feature options_feature;\n\n    LV2_Extension_Data_Feature ext_data;\n};\n\nconstexpr int FEATURE_LIST_SIZE = 11;\n\n/**\n * @brief LV2 depends on a \"GOD\" struct/class per plugin instance,\n * which it passes around with pointers in the various callbacks.\n * LV2Model is this class - to the extent needed for Lilv.\n */\nclass Model\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(Model);\n\n    Model(float sample_rate, LV2_Wrapper* wrapper, LilvWorld* world);\n    ~Model();\n\n    ProcessorReturnCode load_plugin(const LilvPlugin* plugin_handle,\n                                    double sample_rate);\n\n    // Warning: LV2 / Lilv require this list to be null-terminated.\n    // So remember to check for null when iterating over it!\n    std::array<const LV2_Feature*, FEATURE_LIST_SIZE>* host_feature_list();\n\n    LilvWorld* lilv_world();\n\n    LilvInstance* plugin_instance();\n\n    const LilvPlugin* plugin_class();\n\n    int midi_buffer_size() const;\n    float sample_rate() const;\n\n    Port* get_port(int index);\n    void add_port(Port port);\n    int port_count();\n\n    const HostNodes* nodes();\n\n    const LV2_URIDs& urids();\n\n    LV2_URID_Map& get_map();\n    LV2_URID_Unmap& get_unmap();\n\n    LV2_URID map(const char* uri);\n    const char* unmap(LV2_URID urid);\n\n    LV2_Atom_Forge& forge();\n\n    int plugin_latency() const;\n    void set_plugin_latency(int latency);\n\n    void set_control_input_index(int index);\n\n    bool update_requested() const;\n    void request_update();\n    void clear_update_request();\n\n    State* state();\n\n    void set_play_state(PlayState play_state);\n    PlayState play_state() const;\n\n    std::string temp_dir();\n\n    std::string save_dir();\n    void set_save_dir(const std::string& save_dir);\n\n    bool buf_size_set() const;\n\n    std::vector<ControlID>& controls();\n\n    uint32_t position() const;\n    void set_position(uint32_t position);\n\n    float bpm() const;\n    void set_bpm(float bpm);\n\n    bool rolling() const;\n    void set_rolling(bool rolling);\n\n    std::pair<LilvState*, bool> state_to_set();\n    void set_state_to_set(LilvState* state_to_set, bool delete_after_use);\n\n    int input_audio_channel_count() const;\n    int output_audio_channel_count() const;\n\n    bool exit {false}; ///< True iff execution is finished\n\n    Worker* worker();\n    Worker* state_worker();\n\n    bool safe_restore();\n\n    LV2_Wrapper* wrapper();\n\nprivate:\n    bool _create_ports(const LilvPlugin* plugin);\n    Port _create_port(const LilvPlugin* plugin, int port_index, float default_value);\n\n    void _initialize_map_feature();\n    void _initialize_unmap_feature();\n    void _initialize_log_feature();\n    void _initialize_urid_symap();\n\n    void _initialize_make_path_feature();\n\n    void _initialize_worker_feature();\n    void _initialize_safe_restore_feature();\n    void _initialize_options_feature();\n\n    void _create_controls(bool writable);\n\n    void _initialize_host_feature_list();\n\n    bool _check_for_required_features(const LilvPlugin* plugin);\n\n    /** Return true if Sushi supports the given feature. */\n    bool _feature_is_supported(const std::string& uri);\n\n    std::vector<ControlID> _controls;\n\n    bool _buf_size_set{false};\n\n    std::string _temp_dir;\n    std::string _save_dir;\n\n    PlayState _play_state{PlayState::PAUSED};\n\n    std::unique_ptr<State> _lv2_state;\n\n    bool _request_update{false};\n\n    int _control_input_index{0};\n\n    int _plugin_latency{0};\n\n    LV2_Atom_Forge _forge;\n\n    LV2_URID_Map _map;\n    LV2_URID_Unmap _unmap;\n\n    lv2_host::Symap* _symap{nullptr};\n    std::mutex _symap_lock;\n\n    LV2_URIDs _urids;\n\n    float _sample_rate{0};\n\n    LV2_Wrapper* _wrapper{nullptr};\n\n    LilvWorld* _world{nullptr};\n    std::unique_ptr<HostNodes> _nodes{nullptr};\n\n    std::vector<Port> _ports;\n\n    const int _buffer_size{AUDIO_CHUNK_SIZE};\n    int _midi_buffer_size{4096};\n\n    int _ui_update_hz{30};\n\n    const LilvPlugin* _plugin_class{nullptr};\n\n    LilvInstance* _plugin_instance{nullptr};\n\n    HostFeatures _features;\n    std::array<const LV2_Feature*, FEATURE_LIST_SIZE> _feature_list;\n\n    uint32_t _position{0};\n    float _bpm{0};\n    bool _rolling{false};\n\n    bool _delete_state_after_use{false};\n    LilvState* _state_to_set{nullptr};\n\n    int _input_audio_channel_count{0};\n    int _output_audio_channel_count{0};\n\n    bool _safe_restore{false};\n\n    std::unique_ptr<Worker> _state_worker;\n    std::unique_ptr<Worker> _worker;\n};\n\n} // end namespace sushi::internal::lv2\n\n#endif // SUSHI_BUILD_WITH_LV2\n\n#endif // SUSHI_LV2_MODEL_H\n"
  },
  {
    "path": "src/library/lv2/lv2_port.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Wrapper for LV2 plugins - Wrapper for LV2 plugins - port.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifdef SUSHI_BUILD_WITH_LV2\n\n#include \"lv2_port.h\"\n\n#include <math.h>\n\n#include \"lv2_model.h\"\n\nnamespace sushi::internal::lv2 {\n\n\nPort::Port(const LilvPlugin* plugin, int port_index, float default_value, Model* model):\n    _index(port_index)\n{\n    _lilv_port = lilv_plugin_get_port_by_index(plugin, port_index);\n\n    _optional = lilv_port_has_property(plugin, _lilv_port, model->nodes()->lv2_connectionOptional);\n\n    /* Set the port flow (input or output) */\n    if (lilv_port_is_a(plugin, _lilv_port, model->nodes()->lv2_InputPort))\n    {\n        _flow = PortFlow::FLOW_INPUT;\n    }\n    else if (lilv_port_is_a(plugin, _lilv_port, model->nodes()->lv2_OutputPort))\n    {\n        _flow = PortFlow::FLOW_OUTPUT;\n    }\n\n    const bool hidden = (_show_hidden == false) &&\n                        lilv_port_has_property(plugin, _lilv_port, model->nodes()->pprops_notOnGUI);\n\n    /* Set control values */\n    if (lilv_port_is_a(plugin, _lilv_port, model->nodes()->lv2_ControlPort))\n    {\n        _type = PortType::TYPE_CONTROL;\n\n        LilvNode* minNode;\n        LilvNode* maxNode;\n        LilvNode* defNode;\n\n        lilv_port_get_range(plugin, _lilv_port, &defNode, &minNode, &maxNode);\n\n        if (defNode != nullptr)\n        {\n            _def = lilv_node_as_float(defNode);\n        }\n\n        if (maxNode != nullptr)\n        {\n            _max = lilv_node_as_float(maxNode);\n        }\n\n        if (minNode != nullptr)\n        {\n            _min = lilv_node_as_float(minNode);\n        }\n\n        _control = isnan(default_value) ? _def : default_value;\n\n        lilv_node_free(minNode);\n        lilv_node_free(maxNode);\n        lilv_node_free(defNode);\n\n        if (hidden == false)\n        {\n            model->controls().emplace_back(ControlID::new_port_control(this, model, _index));\n        }\n    }\n    else if (lilv_port_is_a(plugin, _lilv_port, model->nodes()->lv2_AudioPort))\n    {\n        _type = PortType::TYPE_AUDIO;\n    }\n    else if (lilv_port_is_a(plugin, _lilv_port, model->nodes()->atom_AtomPort))\n    {\n        _type = PortType::TYPE_EVENT;\n    }\n\n    if (model->buf_size_set() == false)\n    {\n        _allocate_port_buffers(model);\n    }\n}\n\nvoid Port::reset_input_buffer()\n{\n    lv2_evbuf_reset(_evbuf, true);\n}\n\nvoid Port::reset_output_buffer()\n{\n    lv2_evbuf_reset(_evbuf, false);\n}\n\nvoid Port::_allocate_port_buffers(Model* model)\n{\n    switch (_type)\n    {\n        case PortType::TYPE_EVENT:\n        {\n            lv2_evbuf_free(_evbuf);\n\n            auto handle = model->get_map().handle;\n\n            _evbuf = lv2_host::lv2_evbuf_new(\n                    model->midi_buffer_size(),\n                    model->get_map().map(handle, lilv_node_as_string(model->nodes()->atom_Chunk)),\n                    model->get_map().map(handle, lilv_node_as_string(model->nodes()->atom_Sequence)));\n\n            lilv_instance_connect_port(\n                    model->plugin_instance(), _index, lv2_evbuf_get_buffer(_evbuf));\n        }\n        default:\n            break;\n    }\n}\n\nfloat Port::min() const\n{\n    return _min;\n}\n\nfloat Port::max() const\n{\n    return _max;\n}\n\nPortFlow Port::flow() const\n{\n    return _flow;\n}\n\nPortType Port::type() const\n{\n    return _type;\n}\n\nconst LilvPort* Port::lilv_port()\n{\n    return _lilv_port;\n}\n\nlv2_host::LV2_Evbuf* Port::evbuf()\n{\n    return _evbuf;\n}\n\nvoid Port::set_control_value(float c)\n{\n    _control = c;\n}\n\nfloat Port::control_value() const\n{\n    return _control;\n}\n\nfloat* Port::control_pointer()\n{\n    return &_control;\n}\n\nbool Port::optional() const\n{\n    return _optional;\n}\n\n} // end namespace sushi::internal::lv2\n\n#endif // SUSHI_BUILD_WITH_LV2\n"
  },
  {
    "path": "src/library/lv2/lv2_port.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Port - internally used class for holding and interacting with a plugin port.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_LV2_PORT_H\n#define SUSHI_LV2_PORT_H\n\n#ifdef SUSHI_BUILD_WITH_LV2\n\n#include <map>\n\n#include <lilv-0/lilv/lilv.h>\n\n#include \"sushi/constants.h\"\n\n#include \"lv2_host/lv2_evbuf.h\"\n\nnamespace sushi::internal::lv2 {\n\nclass Model;\n\nenum class PortFlow\n{\n    FLOW_UNKNOWN,\n    FLOW_INPUT,\n    FLOW_OUTPUT\n};\n\nenum class PortType\n{\n    TYPE_UNKNOWN,\n    TYPE_CONTROL,\n    TYPE_AUDIO,\n    TYPE_EVENT,\n    TYPE_CV\n};\n\nclass Port\n{\npublic:\n    Port(const LilvPlugin* plugin, int port_index, float default_value, Model* model);\n\n    ~Port() = default;\n\n    void reset_input_buffer();\n    void reset_output_buffer();\n\n    PortFlow flow() const;\n\n    PortType type() const;\n\n    const LilvPort* lilv_port();\n\n    float min() const;\n    float max() const;\n\n    lv2_host::LV2_Evbuf* evbuf();\n\n    void set_control_value(float c);\n\n    float control_value() const;\n\n    float* control_pointer();\n\n    bool optional() const;\n\nprivate:\n    /**\n    * @brief Allocates LV2 port buffers (only necessary for MIDI)\n    */\n    void _allocate_port_buffers(Model *model);\n\n    float _control{0.0f}; ///< For control ports, otherwise 0.0f\n\n    const LilvPort* _lilv_port;\n    PortType _type{PortType::TYPE_UNKNOWN};\n    PortFlow _flow{PortFlow::FLOW_UNKNOWN};\n\n    lv2_host::LV2_Evbuf* _evbuf{nullptr}; // For MIDI ports, otherwise NULL\n\n    int _index;\n\n    // For ranges. Only used in control ports.\n    float _def{1.0f};\n    float _max{1.0f};\n    float _min{0.0f};\n\n    bool _show_hidden{true};\n\n    bool _optional{true};\n};\n\n} // end namespace sushi::internal::lv2\n\n#endif // SUSHI_BUILD_WITH_LV2\n\n#endif // SUSHI_LV2_PORT_H\n"
  },
  {
    "path": "src/library/lv2/lv2_processor_factory.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Factory for LV2 processors.\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"lv2_processor_factory.h\"\n\n#ifdef SUSHI_BUILD_WITH_LV2\n#include \"lv2_wrapper.h\"\n#endif\n\nnamespace sushi::internal::lv2 {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"lv2\");\n\n#ifdef SUSHI_BUILD_WITH_LV2\n\nLv2ProcessorFactory::~Lv2ProcessorFactory() = default;\n\nstd::pair<ProcessorReturnCode, std::shared_ptr<Processor>> Lv2ProcessorFactory::new_instance(const PluginInfo& plugin_info,\n                                                                                             HostControl& host_control,\n                                                                                             float sample_rate)\n{\n    auto world = _world.lock();\n    if (!world)\n    {\n        world = std::make_shared<LilvWorldWrapper>();\n        world->create_world();\n        if (world->world() == nullptr)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to initialize Lilv World\");\n            return {ProcessorReturnCode::SHARED_LIBRARY_OPENING_ERROR, nullptr};\n        }\n        _world = world;\n    }\n    auto processor = std::make_shared<lv2::LV2_Wrapper>(host_control, plugin_info.path, world);\n    auto processor_status = processor->init(sample_rate);\n    return {processor_status, processor};\n}\n\n#else //SUSHI_BUILD_WITH_LV2\n\nLv2ProcessorFactory::~Lv2ProcessorFactory() = default;\n\nstd::pair<ProcessorReturnCode, std::shared_ptr<Processor>> Lv2ProcessorFactory::new_instance([[maybe_unused]] const PluginInfo& plugin_info,\n                                                                                             [[maybe_unused]] HostControl& host_control,\n                                                                                             [[maybe_unused]] float sample_rate)\n\n{\n    ELKLOG_LOG_ERROR(\"Sushi was not built with support for LV2 plugins\");\n    return {ProcessorReturnCode::UNSUPPORTED_OPERATION, nullptr};\n}\n\n#endif // SUSHI_BUILD_WITH_LV2\n\n} // end namespace sushi::internal::lv2\n"
  },
  {
    "path": "src/library/lv2/lv2_processor_factory.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Factory for LV2 processors.\n */\n\n#ifndef SUSHI_LV2_PROCESSOR_FACTORY_H\n#define SUSHI_LV2_PROCESSOR_FACTORY_H\n\n#include \"library/base_processor_factory.h\"\n\nnamespace sushi::internal::lv2 {\n\nclass LilvWorldWrapper;\n\nclass Lv2ProcessorFactory : public BaseProcessorFactory\n{\npublic:\n    ~Lv2ProcessorFactory() override;\n\n    std::pair<ProcessorReturnCode, std::shared_ptr<Processor>> new_instance(const PluginInfo& plugin_info,\n                                                                            HostControl& host_control,\n                                                                            float sample_rate) override;\n\nprivate:\n    std::weak_ptr<LilvWorldWrapper> _world;\n};\n\n} // end namespace sushi::internal::lv2\n\n#endif // SUSHI_LV2_PROCESSOR_FACTORY_H\n"
  },
  {
    "path": "src/library/lv2/lv2_state.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include <lilv-0/lilv/lilv.h>\n\n#include \"elklog/static_logger.h\"\n\n#include \"lv2_state.h\"\n\n#include \"lv2_model.h\"\n#include \"lv2_port.h\"\n#include \"lv2_features.h\"\n\nnamespace sushi::internal::lv2 {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"lv2\");\n\n// Callback method - signature as required by Lilv\nstatic int populate_preset_list(Model* model, const LilvNode *node, const LilvNode* title, void* /*data*/)\n{\n    std::string node_string = lilv_node_as_string(node);\n    std::string title_string = lilv_node_as_string(title);\n\n    model->state()->program_names().push_back(std::move(node_string));\n\n    return 0;\n}\n\nState::State(Model* model):\n    _model(model) {}\n\nstd::vector<std::string>& State::program_names()\n{\n    return _program_names;\n}\n\nint State::number_of_programs()\n{\n    return _program_names.size();\n}\n\nint State::current_program_index() const\n{\n    return _current_program_index;\n}\n\nstd::string State::current_program_name()\n{\n    return program_name(_current_program_index);\n}\n\nstd::string State::program_name(int program_index)\n{\n    if (program_index >= 0 && program_index < number_of_programs())\n    {\n        return _program_names[program_index];\n    }\n\n    return \"\";\n}\n\nvoid State::populate_program_list()\n{\n    _load_programs(populate_preset_list, nullptr);\n    // The order of preset names coming from Livl seems to be quite random at times\n    std::sort(_program_names.begin(), _program_names.end());\n}\n\nvoid State::save(const char* dir)\n{\n    _model->set_save_dir(std::string(dir) + \"/\");\n\n    auto state = lilv_state_new_from_instance(\n            _model->plugin_class(), _model->plugin_instance(), &_model->get_map(),\n            _model->temp_dir().c_str(), dir, dir, dir,\n            get_port_value, _model,\n      LV2_STATE_IS_POD|LV2_STATE_IS_PORTABLE, nullptr);\n\n   lilv_state_save(_model->lilv_world(), &_model->get_map(), &_model->get_unmap(), state, nullptr, dir, \"state.ttl\");\n\n   lilv_state_free(state);\n\n    _model->set_save_dir(std::string(\"\"));\n}\n\nstd::vector<std::byte> State::save_binary_state()\n{\n    std::vector<std::byte> binary_state;\n\n    auto state = lilv_state_new_from_instance(\n            _model->plugin_class(), _model->plugin_instance(), &_model->get_map(),\n            nullptr, nullptr, nullptr, nullptr,\n            get_port_value, _model,\n            LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE, nullptr);\n\n    auto serial_state = lilv_state_to_string(_model->lilv_world(), &_model->get_map(),\n                                             &_model->get_unmap(), state, LV2_STATE_URI, nullptr);\n\n    if (serial_state)\n    {\n        // TODO: Investigate if it is safe to move data into the vector instead. It would save 1 copy but the data couldn't be de-allocated with lilv_free\n        int size = strlen(serial_state);\n        if (size > 0)\n        {\n            // We want to copy the string including the nullterminator to ease decoding later\n            binary_state.assign(reinterpret_cast<std::byte*>(serial_state),\n                                reinterpret_cast<std::byte*>(serial_state + size + 2));\n        }\n        lilv_free(serial_state);\n    }\n    ELKLOG_LOG_ERROR_IF(serial_state == nullptr, \"Failed to get state from plugin\");\n\n    lilv_state_free(state);\n    return binary_state;\n}\n\nvoid State::_load_programs(PresetSink sink, void* data)\n{\n   auto presets = lilv_plugin_get_related(_model->plugin_class(), _model->nodes()->pset_Preset);\n\n   LILV_FOREACH(nodes, i, presets)\n   {\n       auto preset = lilv_nodes_get(presets, i);\n       lilv_world_load_resource(_model->lilv_world(), preset);\n\n       if (sink == nullptr)\n       {\n           continue;\n       }\n\n       auto labels = lilv_world_find_nodes(_model->lilv_world(), preset, _model->nodes()->rdfs_label, nullptr);\n\n       if (labels)\n       {\n           auto label = lilv_nodes_get_first(labels);\n           sink(_model, preset, label, data);\n           lilv_nodes_free(labels);\n       }\n       else\n       {\n           ELKLOG_LOG_ERROR(\"Preset {} has no rdfs:label\", lilv_node_as_string(lilv_nodes_get(presets, i)));\n       }\n   }\n\n   lilv_nodes_free(presets);\n}\n\nvoid State::unload_programs()\n{\n    auto presets = lilv_plugin_get_related(_model->plugin_class(), _model->nodes()->pset_Preset);\n\n    LILV_FOREACH(nodes, i, presets)\n    {\n        auto preset = lilv_nodes_get(presets, i);\n        lilv_world_unload_resource(_model->lilv_world(), preset);\n    }\n\n    lilv_nodes_free(presets);\n}\n\nvoid State::apply_state(LilvState* state, bool delete_after_use)\n{\n    bool must_pause = !_model->safe_restore() && _model->play_state() == PlayState::RUNNING;\n\n    if (state)\n    {\n        if (must_pause)\n        {\n            _model->set_play_state(PlayState::PAUSE_REQUESTED);\n            _model->set_state_to_set(state, delete_after_use);\n        }\n        else\n        {\n            auto feature_list = _model->host_feature_list();\n            lilv_state_restore(state, _model->plugin_instance(), set_port_value, _model, 0, feature_list->data());\n            _model->request_update();\n            if (delete_after_use)\n            {\n                lilv_free(state);\n            }\n        }\n    }\n}\n\nbool State::apply_program(int program_index)\n{\n    if (program_index < number_of_programs())\n    {\n        auto presetNode = lilv_new_uri(_model->lilv_world(), _program_names[program_index].c_str());\n        apply_program(presetNode);\n        lilv_node_free(presetNode);\n\n        _current_program_index = program_index;\n\n        return true;\n    }\n\n    return false;\n}\n\nvoid State::apply_program(const LilvNode* preset)\n{\n    _set_preset(lilv_state_new_from_world(_model->lilv_world(), &_model->get_map(), preset));\n    apply_state(_preset, false);\n}\n\nvoid State::_set_preset(LilvState* new_preset)\n{\n    if (_preset != nullptr)\n    {\n        lilv_state_free(_preset);\n    }\n\n    _preset = new_preset;\n}\n\nint State::save_program(const char* dir, const char* uri, const char* label, const char* filename)\n{\n   auto state = lilv_state_new_from_instance(\n            _model->plugin_class(), _model->plugin_instance(), &_model->get_map(),\n            _model->temp_dir().c_str(), dir, dir, dir,\n            get_port_value, _model,\n      LV2_STATE_IS_POD|LV2_STATE_IS_PORTABLE, nullptr);\n\n   if (label)\n   {\n       lilv_state_set_label(state, label);\n   }\n\n   int ret = lilv_state_save(_model->lilv_world(), &_model->get_map(), &_model->get_unmap(), state, uri, dir, filename);\n\n    _set_preset(state);\n\n   return ret;\n}\n\nbool State::delete_current_program()\n{\n   if (_preset == nullptr)\n   {\n       return false;\n   }\n\n   lilv_world_unload_resource(_model->lilv_world(), lilv_state_get_uri(_preset));\n   lilv_state_delete(_model->lilv_world(), _preset);\n    _set_preset(nullptr);\n\n   return true;\n}\n\n// This one has a signature as required by Lilv.\nconst void* get_port_value(const char* port_symbol, void* user_data, uint32_t* size, uint32_t* type)\n{\n    auto model = static_cast<Model*>(user_data);\n    auto port = port_by_symbol(model, port_symbol);\n\n    if (port && port->flow() == PortFlow::FLOW_INPUT && port->type() == PortType::TYPE_CONTROL)\n    {\n        *size = sizeof(float);\n        *type = model->forge().Float;\n        return port->control_pointer();\n    }\n\n    *size = *type = 0;\n\n    return nullptr;\n}\n\n// This one has a signature as required by Lilv.\nvoid set_port_value(const char* port_symbol,\n                    void* user_data,\n                    const void* value,\n// size was unused also in the JALV equivalent of this method.\n// Seems unneeded, but required by callback signature.\n                    uint32_t /*size*/,\n                    uint32_t type)\n{\n    auto model = static_cast<Model*>(user_data);\n    auto port = port_by_symbol(model, port_symbol);\n\n    if (port == nullptr)\n    {\n        ELKLOG_LOG_DEBUG(\"error: Preset port `{}' is missing\", port_symbol);\n        return;\n    }\n\n    float fvalue;\n\n    const auto& forge = model->forge();\n\n    if (type == forge.Float)\n    {\n        fvalue = *static_cast<const float*>(value);\n    }\n    else if (type == forge.Double)\n    {\n        fvalue = *static_cast<const double*>(value);\n    }\n    else if (type == forge.Int)\n    {\n        fvalue = *static_cast<const int32_t*>(value);\n    }\n    else if (type == forge.Long)\n    {\n        fvalue = *static_cast<const int64_t*>(value);\n    }\n    else\n    {\n        ELKLOG_LOG_DEBUG(\"error: Preset {} value has bad type {}\",\n                port_symbol,\n                model->get_unmap().unmap(model->get_unmap().handle, type));\n\n        return;\n    }\n\n    if (model->play_state() != PlayState::RUNNING)\n    {\n        // Set value on port directly:\n        port->set_control_value(fvalue);\n    }\n}\n\n} // end namespace sushi::internal::lv2\n"
  },
  {
    "path": "src/library/lv2/lv2_state.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief State - internally used class for the storage and manipulation of LV2 presets/states.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_LV2_STATE_H\n#define SUSHI_LV2_STATE_H\n\n#include <vector>\n\n#include \"lv2/lv2plug.in/ns/ext/state/state.h\"\n#include \"lv2/state/state.h\"\n\n#include \"sushi/constants.h\"\n\n#include \"lv2_model.h\"\n#include \"lv2_features.h\"\n\nnamespace sushi::internal::lv2 {\n\nclass State\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(State);\n\n    explicit State(Model* model);\n    ~State() = default;\n\n    void save(const char *dir);\n\n    std::vector<std::byte> save_binary_state();\n\n    void unload_programs();\n\n    void apply_state(LilvState* state, bool delete_after_use);\n\n    bool apply_program(int program_index);\n\n    void apply_program(const LilvNode* preset);\n\n    int save_program(const char* dir, const char* uri, const char* label, const char* filename);\n\n    bool delete_current_program();\n\n    std::vector<std::string>& program_names();\n\n    void populate_program_list();\n\n    int number_of_programs();\n\n    int current_program_index() const;\n\n    std::string current_program_name();\n\n    std::string program_name(int program_index);\n    \nprivate:\n    void _set_preset(LilvState* new_preset);\n\n    void _load_programs(PresetSink sink, void* data);\n\n    std::vector<std::string> _program_names;\n    int _current_program_index{0};\n\n    LilvState* _preset {nullptr}; // Naked pointer because Lilv manages lifetime.\n\n    Model* _model;\n};\n\n// LV2 callbacks:\nconst void* get_port_value(const char* port_symbol, void* user_data, uint32_t *size, uint32_t* type);\nvoid set_port_value(const char* port_symbol, void* user_data, const void* value, uint32_t size, uint32_t type);\n\n} // end namespace sushi::internal::lv2\n\n#endif\n"
  },
  {
    "path": "src/library/lv2/lv2_worker.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include \"lv2_worker.h\"\n#include \"lv2_wrapper.h\"\n\nnamespace sushi::internal::lv2 {\n\n/////////////////////////////////////////////////////////////////////////////////////////////////\n// Static methods:\n/////////////////////////////////////////////////////////////////////////////////////////////////\n\n// With the Guitarix plugins, which are the only worker-thread plugins tested,\n// This does not seem to be used - it never gets invoked, neither in Sushi nor in Jalv.\nstatic LV2_Worker_Status lv2_worker_respond(LV2_Worker_Respond_Handle handle, uint32_t size, const void* data)\n{\n    auto worker = static_cast<Worker*>(handle);\n\n    Lv2FifoItem response;\n    response.size = size;\n\n    memcpy(response.block.data(), data, size);\n\n    worker->responses().push(response);\n\n    return LV2_WORKER_SUCCESS;\n}\n\nLV2_Worker_Status Worker::schedule(LV2_Worker_Schedule_Handle handle, uint32_t size, const void* data)\n{\n    auto worker = static_cast<Worker*>(handle);\n    auto model = worker->_model;\n    auto wrapper = model->wrapper();\n\n    if (worker->_threaded)\n    {\n        // Schedule a request to be executed by the worker thread\n\n        Lv2FifoItem request;\n        request.size = size;\n\n        if (size > request.block.size())\n        {\n            return LV2_WORKER_ERR_NO_SPACE;\n        }\n\n        memcpy(request.block.data(), data, size);\n        if (!worker->requests().push(request))\n        {\n            return LV2_WORKER_ERR_NO_SPACE;\n        }\n        wrapper->request_worker_callback(&LV2_Wrapper::worker_callback);\n    }\n    else\n    {\n        // Execute work immediately in this thread\n        std::unique_lock<std::mutex> lock(worker->_work_lock);\n\n        worker->iface()->work(\n                model->plugin_instance()->lv2_handle,\n                lv2_worker_respond,\n                worker,\n                size,\n                data);\n    }\n    return LV2_WORKER_SUCCESS;\n}\n\n/////////////////////////////////////////////////////////////////////////////////////////////////\n\nWorker::Worker(Model* model)\n{\n    _model = model;\n}\n\nvoid Worker::init(const LV2_Worker_Interface* iface, bool threaded)\n{\n    _iface = iface;\n    _threaded = threaded;\n    _request.resize(4096);\n    _response.resize(4096);\n}\n\nvoid Worker::worker_func()\n{\n    if (_model->exit == true)\n    {\n        return;\n    }\n\n    std::unique_lock<std::mutex> lock(_work_lock);\n\n    Lv2FifoItem request;\n\n    _requests.pop(request);\n\n    memcpy(_request.data(), request.block.data(), request.size);\n\n    _iface->work(\n            _model->plugin_instance()->lv2_handle,\n            lv2_worker_respond,\n            this,\n            request.size,\n            _request.data());\n}\n\nvoid Worker::emit_responses(LilvInstance* instance)\n{\n    while (_responses.wasEmpty() == false)\n    {\n        Lv2FifoItem response;\n\n        _responses.pop(response);\n\n        memcpy(_response.data(), response.block.data(), response.size);\n\n        _iface->work_response(instance->lv2_handle, response.size, _response.data());\n    }\n}\n\nLv2WorkerFifo& Worker::requests()\n{\n    return _requests;\n}\n\nLv2WorkerFifo& Worker::responses()\n{\n    return _responses;\n}\n\nconst LV2_Worker_Interface* Worker::iface()\n{\n    return _iface;\n}\n\n} // end namespace sushi::internal::lv2\n"
  },
  {
    "path": "src/library/lv2/lv2_worker.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#ifndef SUSHI_LV2_WORKER_H\n#define SUSHI_LV2_WORKER_H\n\n#ifdef SUSHI_BUILD_WITH_LV2\n\n#include \"fifo/circularfifo_memory_relaxed_aquire_release.h\"\n\n#include \"lv2/worker/worker.h\"\n#include \"lv2_model.h\"\n\nnamespace sushi::internal::lv2 {\n\nconstexpr int WORKER_FIFO_SIZE = 16;\nconstexpr int WORKER_REQUEST_SIZE = 1024;\n\nstruct Lv2FifoItem\n{\n    int size{0};\n\n    // The zix ring buffer does not enforce a fixed block size.\n    // Instead, it is 4096 bytes in total for the buffer.\n    // Each entry is a size uint, followed by as many bytes as defined in that.\n    // So the safest thing would be 4096-4 really.\n    std::array<std::byte, WORKER_REQUEST_SIZE> block;\n};\n\nusing Lv2WorkerFifo = memory_relaxed_aquire_release::CircularFifo<Lv2FifoItem, WORKER_FIFO_SIZE>;\n\nclass Worker\n{\npublic:\n    explicit Worker(Model* model);\n    ~Worker() = default;\n\n    void init(const LV2_Worker_Interface* iface, bool threaded);\n\n    void emit_responses(LilvInstance* instance);\n\n    Lv2WorkerFifo& requests();\n\n    Lv2WorkerFifo& responses();\n\n    const LV2_Worker_Interface* iface();\n\n    void worker_func();\n\n    // Signature is as required by LV2 api.\n    static LV2_Worker_Status schedule(LV2_Worker_Schedule_Handle handle, uint32_t size, const void *data);\n\nprivate:\n    std::mutex _work_lock;\n\n    const LV2_Worker_Interface* _iface = nullptr;\n\n    Lv2WorkerFifo _requests;\n    Lv2WorkerFifo _responses;\n\n    std::vector<std::byte> _request;\n    std::vector<std::byte> _response;\n\n    Model* _model{nullptr};\n    bool _threaded{false};\n};\n\n} // end namespace sushi::internal::lv2\n\n#endif // SUSHI_BUILD_WITH_LV2\n\n#endif // SUSHI_LV2_WORKER_H"
  },
  {
    "path": "src/library/lv2/lv2_wrapper.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @Brief Wrapper for LV2 plugins - Wrapper for LV2 plugins.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <exception>\n#include <cmath>\n\n#include <twine/twine.h>\n\n#include \"elklog/static_logger.h\"\n\n#include \"lv2_wrapper.h\"\n\n#include \"lv2_port.h\"\n#include \"lv2_state.h\"\n#include \"lv2_control.h\"\n\n#include \"lv2_worker.h\"\n\nnamespace sushi::internal::lv2 {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"lv2\");\n\nLilvWorldWrapper::~LilvWorldWrapper()\n{\n    if (_world)\n    {\n        lilv_world_free(_world);\n    }\n}\n\nbool LilvWorldWrapper::create_world()\n{\n    assert(_world == nullptr);\n\n    _world = lilv_world_new();\n    if (_world)\n    {\n        lilv_world_load_all(_world);\n    }\n    return _world;\n}\n\nLilvWorld* LilvWorldWrapper::world()\n{\n    return _world;\n}\n\nLV2_Wrapper::LV2_Wrapper(HostControl host_control,\n                         const std::string& lv2_plugin_uri,\n                         std::shared_ptr<LilvWorldWrapper> world):\n                         Processor(host_control),\n                         _plugin_path(lv2_plugin_uri),\n                         _world(world)\n{\n    _max_input_channels = LV2_WRAPPER_MAX_N_CHANNELS;\n    _max_output_channels = LV2_WRAPPER_MAX_N_CHANNELS;\n}\n\nProcessorReturnCode LV2_Wrapper::init(float sample_rate)\n{\n    _control_output_refresh_interval = static_cast<int>(std::round(sample_rate / CONTROL_OUTPUT_REFRESH_RATE));\n\n    _model = std::make_unique<Model>(sample_rate, this, _world->world());\n\n    _lv2_pos = reinterpret_cast<LV2_Atom*>(pos_buf);\n\n    auto library_handle = _plugin_handle_from_URI(_plugin_path);\n\n    if (library_handle == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to load LV2 plugin - handle not recognized.\");\n        return ProcessorReturnCode::SHARED_LIBRARY_OPENING_ERROR;\n    }\n\n    auto loading_return_code = _model->load_plugin(library_handle, sample_rate);\n\n    if (loading_return_code != ProcessorReturnCode::OK)\n    {\n        return loading_return_code;\n    }\n\n    // Channel setup derived from ports:\n    _max_input_channels = _model->input_audio_channel_count();\n    _max_output_channels = _model->output_audio_channel_count();\n\n    _fetch_plugin_name_and_label();\n\n    if (_register_parameters() == false) // Register internal parameters\n    {\n        ELKLOG_LOG_ERROR(\"Failed to allocate LV2 feature list.\");\n        return ProcessorReturnCode::PARAMETER_ERROR;\n    }\n\n    _model->set_play_state(PlayState::RUNNING);\n\n    return ProcessorReturnCode::OK;\n}\n\nvoid LV2_Wrapper::_fetch_plugin_name_and_label()\n{\n    const auto uri_node = lilv_plugin_get_uri(_model->plugin_class());\n    const std::string uri_as_string = lilv_node_as_string(uri_node);\n    set_name(uri_as_string);\n\n    auto label_node = lilv_plugin_get_name(_model->plugin_class());\n    const std::string label_as_string = lilv_node_as_string(label_node);\n    set_label(label_as_string);\n    lilv_free(label_node);\n}\n\nvoid LV2_Wrapper::configure(float /*sample_rate*/)\n{\n    ELKLOG_LOG_WARNING(\"LV2 does not support altering the sample rate after initialization.\");\n}\n\nconst ParameterDescriptor* LV2_Wrapper::parameter_from_id(ObjectId id) const\n{\n    auto descriptor = _parameters_by_lv2_id.find(id);\n    if (descriptor !=  _parameters_by_lv2_id.end())\n    {\n        return descriptor->second;\n    }\n    return nullptr;\n}\n\nstd::pair<ProcessorReturnCode, float> LV2_Wrapper::parameter_value(ObjectId parameter_id) const\n{\n    auto parameter = parameter_from_id(parameter_id);\n\n    if (parameter == nullptr)\n    {\n        return {ProcessorReturnCode::PARAMETER_NOT_FOUND, 0.0f};\n    }\n\n    // All parameters registered in the wrapper were of type FloatParameterDescriptor\n    if (parameter->type() != ParameterType::FLOAT)\n    {\n        return {ProcessorReturnCode::PARAMETER_ERROR, 0.0f};\n    }\n\n    const int index = static_cast<int>(parameter_id);\n\n    if (index < _model->port_count())\n    {\n        auto port = _model->get_port(index);\n\n        if (port != nullptr)\n        {\n            float value = port->control_value();\n            float min = parameter->min_domain_value();\n            float max = parameter->max_domain_value();\n\n            float value_normalized = _to_normalized(value, min, max);\n\n            return {ProcessorReturnCode::OK, value_normalized};\n        }\n    }\n\n    return {ProcessorReturnCode::PARAMETER_NOT_FOUND, 0.0f};\n}\n\nstd::pair<ProcessorReturnCode, float> LV2_Wrapper::parameter_value_in_domain(ObjectId parameter_id) const\n{\n    float value = 0.0;\n    const int index = static_cast<int>(parameter_id);\n\n    if (index < _model->port_count())\n    {\n        auto port = _model->get_port(index);\n\n        if (port != nullptr)\n        {\n            value = port->control_value();\n            return {ProcessorReturnCode::OK, value};\n        }\n    }\n\n    return {ProcessorReturnCode::PARAMETER_NOT_FOUND, value};\n}\n\nstd::pair<ProcessorReturnCode, std::string> LV2_Wrapper::parameter_value_formatted(ObjectId parameter_id) const\n{\n    auto value_tuple = parameter_value_in_domain(parameter_id);\n\n    if (value_tuple.first == ProcessorReturnCode::OK)\n    {\n        std::string parsed_value = std::to_string(value_tuple.second);\n        return {ProcessorReturnCode::OK, parsed_value};\n    }\n\n    return {ProcessorReturnCode::PARAMETER_NOT_FOUND, \"\"};\n}\n\nbool LV2_Wrapper::supports_programs() const\n{\n    return _model->state()->number_of_programs() > 0;\n}\n\nint LV2_Wrapper::program_count() const\n{\n    return _model->state()->number_of_programs();\n}\n\nint LV2_Wrapper::current_program() const\n{\n    if (this->supports_programs())\n    {\n        return _model->state()->current_program_index();\n    }\n\n    return 0;\n}\n\nstd::string LV2_Wrapper::current_program_name() const\n{\n   return _model->state()->current_program_name();\n}\n\nstd::pair<ProcessorReturnCode, std::string> LV2_Wrapper::program_name(int program) const\n{\n    if (this->supports_programs())\n    {\n        if (program < _model->state()->number_of_programs())\n        {\n            std::string name = _model->state()->program_name(program);\n            return {ProcessorReturnCode::OK, name};\n        }\n    }\n\n    return {ProcessorReturnCode::ERROR, \"\"};\n}\n\nstd::pair<ProcessorReturnCode, std::vector<std::string>> LV2_Wrapper::all_program_names() const\n{\n    if (this->supports_programs() == false)\n    {\n        return {ProcessorReturnCode::UNSUPPORTED_OPERATION, std::vector<std::string>()};\n    }\n\n    std::vector<std::string> programs(_model->state()->program_names().begin(),\n                                      _model->state()->program_names().end());\n\n    return {ProcessorReturnCode::OK, programs};\n}\n\nProcessorReturnCode LV2_Wrapper::set_program(int program)\n{\n    if (this->supports_programs() && program < _model->state()->number_of_programs())\n    {\n        bool succeeded = _model->state()->apply_program(program);\n\n        if (succeeded)\n        {\n            return ProcessorReturnCode::OK;\n        }\n        else\n        {\n            return ProcessorReturnCode::ERROR;\n        }\n    }\n\n    return ProcessorReturnCode::UNSUPPORTED_OPERATION;\n}\n\nProcessorReturnCode LV2_Wrapper::set_state(ProcessorState* state, bool realtime_running)\n{\n    if (state->has_binary_data())\n    {\n        _set_binary_state(state);\n        return ProcessorReturnCode::OK;\n    }\n\n    std::unique_ptr<RtState> rt_state;\n    if (realtime_running)\n    {\n        rt_state = std::make_unique<RtState>();\n    }\n\n    if (state->program().has_value())\n    {\n        this->set_program(state->program().value());\n    }\n\n    if (state->bypassed().has_value())\n    {\n        if (realtime_running)\n        {\n            rt_state->set_bypass(state->bypassed().value());\n        }\n        else\n        {\n            _bypass_manager.set_bypass(state->bypassed().value(), _model->sample_rate());\n        }\n    }\n\n    for (const auto& param : state->parameters())\n    {\n        int id = param.first;\n        float value = param.second;\n\n        auto parameter = parameter_from_id(id);\n\n        if (parameter)\n        {\n            auto port = _model->get_port(parameter->id());\n\n            float min = parameter->min_domain_value();\n            float max = parameter->max_domain_value();\n            auto value_in_domain = _to_domain(value, min, max);\n\n            // We can save some time for the audio thread if we do this pre-scaling here\n            // for the realtime case too, even though the values are applied during the\n            // next audio process call and not here.\n            if (realtime_running)\n            {\n                rt_state->add_parameter_change(parameter->id(), value_in_domain);\n            }\n            else\n            {\n                port->set_control_value(value_in_domain);\n            }\n        }\n    }\n\n    if (realtime_running)\n    {\n        _host_control.post_event(std::make_unique<RtStateEvent>(this->id(), std::move(rt_state), IMMEDIATE_PROCESS));\n    }\n    else\n    {\n        _host_control.post_event(std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_UPDATED,\n                                                                               this->id(), 0, IMMEDIATE_PROCESS));\n    }\n\n    return ProcessorReturnCode::OK;\n}\n\nProcessorState LV2_Wrapper::save_state() const\n{\n    ProcessorState state;\n    state.set_binary_data(_model->state()->save_binary_state());\n    return state;\n}\n\nPluginInfo LV2_Wrapper::info() const\n{\n    PluginInfo info;\n    info.type = PluginType::LV2;\n    info.path = _plugin_path;\n    return info;\n}\n\nbool LV2_Wrapper::_register_parameters()\n{\n    bool param_inserted_ok = true;\n\n    for (int _pi = 0; _pi < _model->port_count(); ++_pi)\n    {\n        auto current_port = _model->get_port(_pi);\n\n        if (current_port->type() == PortType::TYPE_CONTROL)\n        {\n            // Here I need to get the name of the port.\n            auto name_node = lilv_port_get_name(_model->plugin_class(), current_port->lilv_port());\n            int port_index = lilv_port_get_index(_model->plugin_class(), current_port->lilv_port());\n\n            assert(port_index == _pi); // This should only fail is the plugin's .ttl file is incorrect.\n\n            const std::string name_as_string = lilv_node_as_string(name_node);\n            const std::string param_unit;\n\n            auto direction = Direction::AUTOMATABLE;\n\n            if (current_port->flow() == PortFlow::FLOW_OUTPUT)\n            {\n                direction = Direction::OUTPUT;\n                ELKLOG_LOG_INFO(\"LV2 Plugin: {}, parameter: {} is output only, so not automatable.\", name(), name_as_string);\n            }\n\n            param_inserted_ok = register_parameter(new FloatParameterDescriptor(name_as_string, // name\n                                                                                name_as_string, // label\n                                                                                param_unit, // PARAMETER UNIT\n                                                                                current_port->min(), // range min\n                                                                                current_port->max(), // range max\n                                                                                direction,\n                                                                                nullptr), // ParameterPreProcessor\n                                                   static_cast<ObjectId>(port_index)); // Registering the ObjectID as the LV2 Port index.\n\n            if (param_inserted_ok)\n            {\n                ELKLOG_LOG_INFO(\"LV2 Plugin: {}, registered parameter: {}\", name(), name_as_string);\n            }\n            else\n            {\n                ELKLOG_LOG_ERROR(\"Plugin: {}, Error while registering parameter: {}\", name(), name_as_string);\n            }\n\n            lilv_node_free(name_node);\n        }\n    }\n\n    /* Create a \"backwards map\" from LV2 parameter ids to parameter indices.\n     * LV2 parameter ports have an integer ID, assigned in the ttl file.\n     * While often it starts from 0 and goes up to n-1 parameters, there is no\n     * guarantee. Very often this is not true, when in the ttl, the parameter ports,\n     * are preceded by other types of ports in the list (i.e. audio/midi i/o).\n     */\n    for (auto param : this->all_parameters())\n    {\n        _parameters_by_lv2_id[param->id()] = param;\n    }\n\n    return param_inserted_ok;\n}\n\nvoid LV2_Wrapper::process_event(const RtEvent& event)\n{\n    if (event.type() == RtEventType::FLOAT_PARAMETER_CHANGE)\n    {\n        auto typed_event = event.parameter_change_event();\n        auto parameter_id = typed_event->param_id();\n\n        auto parameter = parameter_from_id(parameter_id);\n\n        const int port_index = static_cast<int>(parameter_id);\n        assert(port_index < _model->port_count());\n\n        auto port = _model->get_port(port_index);\n\n        auto value = typed_event->value();\n\n        float min = parameter->min_domain_value();\n        float max = parameter->max_domain_value();\n\n        auto value_in_domain = _to_domain(value, min, max);\n        port->set_control_value(value_in_domain);\n    }\n    else if (is_keyboard_event(event))\n    {\n        if (_incoming_event_queue.push(event) == false)\n        {\n            ELKLOG_LOG_DEBUG(\"Plugin: {}, MIDI queue Overflow!\", name());\n        }\n    }\n    else if (event.type() == RtEventType::SET_BYPASS)\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        _bypass_manager.set_bypass(bypassed, _model->sample_rate());\n    }\n    else if (event.type() == RtEventType::SET_STATE)\n    {\n        auto state = event.processor_state_event()->state();\n        if (state->bypassed().has_value())\n        {\n            _bypass_manager.set_bypass(*state->bypassed(), _model->sample_rate());\n        }\n\n        for (const auto& parameter : state->parameters())\n        {\n            // These parameter values are pre-scaled and don't need to be converted to domain values\n            auto port = _model->get_port(parameter.first);\n            port->set_control_value(parameter.second);\n        }\n        async_delete(state);\n        notify_state_change_rt();\n    }\n}\n\nvoid LV2_Wrapper::_update_transport()\n{\n    auto transport = _host_control.transport();\n\n    const bool rolling = transport->playing();\n    const float beats_per_minute = transport->current_tempo();\n    const auto ts = transport->time_signature();\n    const int beats_per_bar = ts.numerator;\n    const int beat_type = ts.denominator;\n    const double current_bar_beats = transport->current_bar_beats();\n    const int32_t bar = transport->current_bar_start_beats() / current_bar_beats;\n    const uint32_t frame = transport->current_samples() / AUDIO_CHUNK_SIZE;\n\n    // If transport state is not as expected, then something has changed.\n    _xport_changed = (rolling != _model->rolling() ||\n                      frame != _model->position() ||\n                      beats_per_minute != _model->bpm());\n\n    if (_xport_changed)\n    {\n        // Build an LV2 position object to report change to plugin.\n        lv2_atom_forge_set_buffer(&_model->forge(), pos_buf, sizeof(pos_buf));\n        LV2_Atom_Forge* forge = &_model->forge();\n\n        LV2_Atom_Forge_Frame frame_atom;\n        lv2_atom_forge_object(forge, &frame_atom, 0, _model->urids().time_Position);\n        lv2_atom_forge_key(forge, _model->urids().time_frame);\n        lv2_atom_forge_long(forge, frame);\n        lv2_atom_forge_key(forge, _model->urids().time_speed);\n        lv2_atom_forge_float(forge, rolling ? 1.0 : 0.0);\n\n        lv2_atom_forge_key(forge, _model->urids().time_barBeat);\n        lv2_atom_forge_float(forge, current_bar_beats);\n\n        lv2_atom_forge_key(forge, _model->urids().time_bar);\n        lv2_atom_forge_long(forge, bar - 1);\n\n        lv2_atom_forge_key(forge, _model->urids().time_beatUnit);\n        lv2_atom_forge_int(forge, beat_type);\n\n        lv2_atom_forge_key(forge, _model->urids().time_beatsPerBar);\n        lv2_atom_forge_float(forge, beats_per_bar);\n\n        lv2_atom_forge_key(forge, _model->urids().time_beatsPerMinute);\n        lv2_atom_forge_float(forge, beats_per_minute);\n    }\n\n    // Update model transport state to expected values for next cycle.\n    _model->set_position(rolling ? frame + AUDIO_CHUNK_SIZE : frame);\n    _model->set_bpm(beats_per_minute);\n    _model->set_rolling(rolling);\n}\n\nvoid LV2_Wrapper::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    if (_bypass_manager.should_process() == false)\n    {\n         bypass_process(in_buffer, out_buffer);\n        _flush_event_queue();\n    }\n    else\n    {\n        switch (_model->play_state())\n        {\n            case PlayState::PAUSE_REQUESTED:\n            {\n                _model->set_play_state(PlayState::PAUSED);\n\n                request_non_rt_task(&restore_state_callback);\n                break;\n            }\n            case PlayState::PAUSED:\n            {\n                _flush_event_queue();\n                return;\n            }\n            default:\n                break;\n        }\n\n        _update_transport();\n\n        _map_audio_buffers(in_buffer, out_buffer);\n\n        _deliver_inputs_to_plugin();\n\n        lilv_instance_run(_model->plugin_instance(), AUDIO_CHUNK_SIZE);\n\n        /* Process any worker replies. */\n        if (_model->state_worker() != nullptr)\n        {\n            _model->state_worker()->emit_responses(_model->plugin_instance());\n        }\n\n        _model->worker()->emit_responses(_model->plugin_instance());\n\n        _deliver_outputs_from_plugin(false);\n\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer, _current_input_channels, _current_output_channels);\n        }\n    }\n}\n\nvoid LV2_Wrapper::_restore_state_callback(EventId)\n{\n    /* Note that this doesn't handle multiple requests at once.\n     * Currently for the Pause functionality it is fine,\n     * but if extended to support other use it may note be. */\n\n    auto [state_to_set, delete_after_use] = _model->state_to_set();\n    if (state_to_set)\n    {\n        auto feature_list = _model->host_feature_list();\n\n        lilv_state_restore(state_to_set,\n                           _model->plugin_instance(),\n                           set_port_value,\n                           _model.get(),\n                           0,\n                           feature_list->data());\n\n        _model->set_state_to_set(nullptr, false);\n        _model->request_update();\n        _model->set_play_state(PlayState::RUNNING);\n\n        if (delete_after_use)\n        {\n            lilv_free(state_to_set);\n        }\n    }\n}\n\nvoid LV2_Wrapper::_worker_callback(EventId)\n{\n    _model->worker()->worker_func();\n}\n\nvoid LV2_Wrapper::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    if (enabled)\n    {\n        lilv_instance_activate(_model->plugin_instance());\n    }\n    else\n    {\n        lilv_instance_deactivate(_model->plugin_instance());\n    }\n}\n\nvoid LV2_Wrapper::set_bypassed(bool bypassed)\n{\n    assert(twine::is_current_thread_realtime() == false);\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(),\n                                                                       bypassed,\n                                                                       IMMEDIATE_PROCESS));\n}\n\nbool LV2_Wrapper::bypassed() const\n{\n    return _bypass_manager.bypassed();\n}\n\nvoid LV2_Wrapper::request_worker_callback(AsyncWorkCallback callback)\n{\n    request_non_rt_task(callback);\n}\n\nvoid LV2_Wrapper::_deliver_inputs_to_plugin()\n{\n    auto instance = _model->plugin_instance();\n\n    for (int p = 0, i = 0, o = 0; p < _model->port_count(); ++p)\n    {\n        auto current_port = _model->get_port(p);\n\n        switch(current_port->type())\n        {\n            case PortType::TYPE_CONTROL:\n                lilv_instance_connect_port(instance, p, current_port->control_pointer());\n                break;\n            case PortType::TYPE_AUDIO:\n                if (current_port->flow() == PortFlow::FLOW_INPUT)\n                    lilv_instance_connect_port(instance, p, _process_inputs[i++]);\n                else\n                    lilv_instance_connect_port(instance, p, _process_outputs[o++]);\n                break;\n            case PortType::TYPE_EVENT:\n                if (current_port->flow() == PortFlow::FLOW_INPUT)\n                {\n                    current_port->reset_input_buffer();\n                    _process_midi_input(current_port);\n\n                }\n                else if (current_port->flow() == PortFlow::FLOW_OUTPUT) // Clear event output for plugin to write to.\n                {\n                    current_port->reset_output_buffer();\n                }\n                break;\n            case PortType::TYPE_CV: // CV Support not yet implemented.\n            case PortType::TYPE_UNKNOWN:\n                assert(false);\n                break;\n            default:\n                lilv_instance_connect_port(instance, p, nullptr);\n        }\n    }\n\n    _model->clear_update_request();\n}\n\nvoid LV2_Wrapper::_deliver_outputs_from_plugin(bool /*send_ui_updates*/)\n{\n    for (int p = 0; p < _model->port_count(); ++p)\n    {\n        auto current_port = _model->get_port(p);\n\n        if (current_port->flow() == PortFlow::FLOW_OUTPUT)\n        {\n            switch(current_port->type())\n            {\n                case PortType::TYPE_CONTROL:\n                    if (lilv_port_has_property(_model->plugin_class(),\n                                               current_port->lilv_port(),\n                                               _model->nodes()->lv2_reportsLatency))\n                    {\n                        if (_model->plugin_latency() != current_port->control_value())\n                        {\n                            _model->set_plugin_latency(current_port->control_value());\n                            // TODO: Introduce latency compensation reporting to Sushi\n                        }\n                    }\n                    else\n                    {\n                        if (_calculate_control_output_trigger())\n                        {\n                            int parameter_id = p; // We use the index as ID.\n                            float normalized_value = _to_normalized(current_port->control_value(),\n                                                                    current_port->min(),\n                                                                    current_port->max());\n                            auto e = RtEvent::make_parameter_change_event(this->id(),\n                                                                          0,\n                                                                          parameter_id,\n                                                                          normalized_value);\n                            output_event(e);\n                        }\n                    }\n                    break;\n                case PortType::TYPE_EVENT:\n                    _process_midi_output(current_port);\n                    break;\n                case PortType::TYPE_UNKNOWN:\n                case PortType::TYPE_AUDIO:\n                case PortType::TYPE_CV:\n                    break;\n            }\n        }\n    }\n}\n\nbool LV2_Wrapper::_calculate_control_output_trigger()\n{\n    bool trigger = false;\n    _control_output_sample_count += AUDIO_CHUNK_SIZE;\n    if (_control_output_sample_count > _control_output_refresh_interval)\n    {\n        _control_output_sample_count -= _control_output_refresh_interval;\n        trigger = true;\n    }\n\n    return trigger;\n}\n\nvoid LV2_Wrapper::_process_midi_output(Port* port)\n{\n    for (auto buf_i = lv2_evbuf_begin(port->evbuf()); lv2_evbuf_is_valid(buf_i); buf_i = lv2_evbuf_next(buf_i))\n    {\n        uint32_t midi_frames, midi_subframes, midi_type, midi_size;\n        uint8_t* midi_body;\n\n        // Get event from LV2 buffer.\n        lv2_evbuf_get(buf_i, &midi_frames, &midi_subframes, &midi_type, &midi_size, &midi_body);\n\n        if (midi_type == _model->urids().midi_MidiEvent)\n        {\n            output_midi_event_as_internal(midi::to_midi_data_byte(midi_body, midi_size), 0);\n        }\n    }\n}\n\nvoid LV2_Wrapper::_process_midi_input(Port* port)\n{\n    auto lv2_evbuf_iterator = lv2_evbuf_begin(port->evbuf());\n\n    // Write transport change event if applicable:\n    if (_xport_changed)\n    {\n        lv2_evbuf_write(&lv2_evbuf_iterator,\n                        0, 0, _lv2_pos->type,\n                        _lv2_pos->size,\n                        (const uint8_t *) LV2_ATOM_BODY(_lv2_pos));\n    }\n\n    auto& urids = _model->urids();\n\n    if (_model->update_requested())\n    {\n        // Plugin state has changed, request an update\n        LV2_Atom_Object atom = {\n                {sizeof(LV2_Atom_Object_Body), urids.atom_Object},\n                {0,urids.patch_Get}};\n\n        lv2_evbuf_write(&lv2_evbuf_iterator, 0, 0,\n                        atom.atom.type, atom.atom.size,\n                        (const uint8_t *) LV2_ATOM_BODY(&atom));\n    }\n\n    // MIDI transfer, from incoming RT event queue into LV2 event buffers:\n    RtEvent rt_event;\n    while (_incoming_event_queue.empty() == false)\n    {\n        if (_incoming_event_queue.pop(rt_event))\n        {\n            MidiDataByte midi_data = _convert_event_to_midi_buffer(rt_event);\n\n            lv2_evbuf_write(&lv2_evbuf_iterator,\n                            rt_event.sample_offset(), // Assuming sample_offset is the timestamp\n                            0, // Subframes\n                            urids.midi_MidiEvent,\n                            midi_data.size(),\n                            midi_data.data());\n        }\n    }\n}\n\nvoid LV2_Wrapper::_flush_event_queue()\n{\n    RtEvent rt_event;\n    while (_incoming_event_queue.empty() == false)\n    {\n        _incoming_event_queue.pop(rt_event);\n    }\n}\n\nMidiDataByte LV2_Wrapper::_convert_event_to_midi_buffer(RtEvent& event)\n{\n    if (event.type() >= RtEventType::NOTE_ON && event.type() <= RtEventType::NOTE_AFTERTOUCH)\n    {\n        auto keyboard_event_ptr = event.keyboard_event();\n\n        switch (keyboard_event_ptr->type())\n        {\n            case RtEventType::NOTE_ON:\n            {\n                return midi::encode_note_on(keyboard_event_ptr->channel(),\n                                                  keyboard_event_ptr->note(),\n                                                  keyboard_event_ptr->velocity());\n            }\n            case RtEventType::NOTE_OFF:\n            {\n                return midi::encode_note_off(keyboard_event_ptr->channel(),\n                                                   keyboard_event_ptr->note(),\n                                                   keyboard_event_ptr->velocity());\n            }\n            case RtEventType::NOTE_AFTERTOUCH:\n            {\n                return midi::encode_poly_key_pressure(keyboard_event_ptr->channel(),\n                                                            keyboard_event_ptr->note(),\n                                                            keyboard_event_ptr->velocity());\n            }\n            default:\n                return {};\n        }\n    }\n    else if (event.type() >= RtEventType::PITCH_BEND && event.type() <= RtEventType::MODULATION)\n    {\n        auto keyboard_common_event_ptr = event.keyboard_common_event();\n\n        switch (keyboard_common_event_ptr->type())\n        {\n            case RtEventType::AFTERTOUCH:\n            {\n                return midi::encode_channel_pressure(keyboard_common_event_ptr->channel(),\n                                                           keyboard_common_event_ptr->value());\n            }\n            case RtEventType::PITCH_BEND:\n            {\n                return midi::encode_pitch_bend(keyboard_common_event_ptr->channel(),\n                                                     keyboard_common_event_ptr->value());\n            }\n            case RtEventType::MODULATION:\n            {\n                return midi::encode_control_change(keyboard_common_event_ptr->channel(),\n                                                         midi::MOD_WHEEL_CONTROLLER_NO,\n                                                         keyboard_common_event_ptr->value());\n            }\n            default:\n                return {};\n        }\n    }\n    else if (event.type() == RtEventType::WRAPPED_MIDI_EVENT)\n    {\n        auto wrapped_midi_event_ptr = event.wrapped_midi_event();\n        return wrapped_midi_event_ptr->midi_data();\n    }\n\n    assert(false); // All cases should have been catered for.\n    return {};\n}\n\nvoid LV2_Wrapper::_map_audio_buffers(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    int i;\n\n    for (i = 0; i < _current_input_channels; ++i)\n    {\n        _process_inputs[i] = const_cast<float*>(in_buffer.channel(i));\n    }\n\n    for (; i <= _max_input_channels; ++i)\n    {\n        _process_inputs[i] = (_dummy_input.channel(0));\n    }\n\n    for (i = 0; i < _current_output_channels; i++)\n    {\n        _process_outputs[i] = out_buffer.channel(i);\n    }\n\n    for (; i <= _max_output_channels; ++i)\n    {\n        _process_outputs[i] = _dummy_output.channel(0);\n    }\n}\n\nvoid LV2_Wrapper::_pause_audio_processing()\n{\n    _previous_play_state = _model->play_state();\n\n    if (_previous_play_state != PlayState::PAUSED)\n    {\n        _model->set_play_state(PlayState::PAUSED);\n    }\n}\n\nvoid LV2_Wrapper::_resume_audio_processing()\n{\n    _model->set_play_state(_previous_play_state);\n}\n\nconst LilvPlugin* LV2_Wrapper::_plugin_handle_from_URI(const std::string& plugin_URI_string)\n{\n    if (plugin_URI_string.empty())\n    {\n        ELKLOG_LOG_ERROR(\"Empty library path\");\n        return nullptr; // Calling dlopen with an empty string returns a handle to the calling\n        // program, which can cause an infinite loop.\n    }\n\n    auto plugins = lilv_world_get_all_plugins(_model->lilv_world());\n    auto plugin_uri = lilv_new_uri(_model->lilv_world(), plugin_URI_string.c_str());\n\n    if (plugin_uri == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Missing plugin URI, try lv2ls to list plugins.\");\n        return nullptr;\n    }\n\n    /* Find plugin */\n    ELKLOG_LOG_INFO(\"Plugin: {}\", lilv_node_as_string(plugin_uri));\n    const auto plugin = lilv_plugins_get_by_uri(plugins, plugin_uri);\n    lilv_node_free(plugin_uri);\n\n    if (plugin == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to find LV2 plugin.\");\n        return nullptr;\n    }\n\n    return plugin;\n}\n\nvoid LV2_Wrapper::_set_binary_state(ProcessorState* state)\n{\n    auto lilv_state = lilv_state_new_from_string(_world->world(),\n                                                 &_model->get_map(),\n                                                 reinterpret_cast<const char*>(state->binary_data().data()));\n\n    if (lilv_state)\n    {\n        auto state_handler = _model->state();\n        state_handler->apply_state(lilv_state, true);\n        _host_control.post_event(std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_UPDATED,\n                                                                               this->id(), 0, IMMEDIATE_PROCESS));\n    }\n    ELKLOG_LOG_ERROR_IF(lilv_state == nullptr, \"Failed to decode lilv state from binary state\");\n}\n\n} // end namespace sushi::internal::lv2\n"
  },
  {
    "path": "src/library/lv2/lv2_wrapper.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n// Parts of the code in this LV2 folder is adapted from the Jalv LV2 example host:\n\n/**\n * Copyright 2007-2016 David Robillard <http://drobilla.net>\n\n * Permission to use, copy, modify, and/or distribute this software for any\n * purpose with or without fee is hereby granted, provided that the above\n * copyright notice and this permission notice appear in all copies.\n\n * THIS SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n */\n\n/**\n * @Brief Wrapper for LV2 plugins - Wrapper for LV2 plugins.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_LV2_PLUGIN_H\n#define SUSHI_LV2_PLUGIN_H\n\n#include <map>\n\n#include \"sushi/constants.h\"\n\n#include \"engine/base_event_dispatcher.h\"\n#include \"library/processor.h\"\n#include \"library/rt_event_fifo.h\"\n#include \"library/midi_encoder.h\"\n#include \"library/midi_decoder.h\"\n\n\n#include \"lv2_model.h\"\n\nnamespace sushi::internal::lv2 {\n\n/* Should match the maximum reasonable number of channels of a plugin */\nconstexpr int LV2_WRAPPER_MAX_N_CHANNELS = MAX_TRACK_CHANNELS;\n\nconstexpr float CONTROL_OUTPUT_REFRESH_RATE = 30.0f;\n\n/**\n * @brief Wrapper around the global LilvWorld instance so that it can be used\n *        with standard c++ smart pointers\n */\nclass LilvWorldWrapper\n{\npublic:\n    ~LilvWorldWrapper();\n\n    bool create_world();\n\n    LilvWorld* world();\n\nprivate:\n    LilvWorld* _world{nullptr};\n};\n\n/**\n * @brief internal wrapper class for loading VST plugins and make them accessible as Processor to the Engine.\n */\n\nclass LV2_Wrapper : public Processor\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(LV2_Wrapper);\n    /**\n     * @brief Create a new Processor that wraps the plugin found in the given path.\n     */\n    LV2_Wrapper(HostControl host_control, const std::string& lv2_plugin_uri, std::shared_ptr<LilvWorldWrapper> world);\n\n    ~LV2_Wrapper() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    // LV2 does not support changing the sample rate after initialization.\n    void configure(float /*sample_rate*/) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    bool bypassed() const override;\n\n    const ParameterDescriptor* parameter_from_id(ObjectId id) const override;\n\n    std::pair<ProcessorReturnCode, float> parameter_value(ObjectId parameter_id) const override;\n\n    std::pair<ProcessorReturnCode, float> parameter_value_in_domain(ObjectId parameter_id) const override;\n\n    std::pair<ProcessorReturnCode, std::string> parameter_value_formatted(ObjectId parameter_id) const override;\n\n    bool supports_programs() const override;\n\n    int program_count() const override;\n\n    int current_program() const override;\n\n    std::string current_program_name() const override;\n\n    std::pair<ProcessorReturnCode, std::string> program_name(int program) const override;\n\n    std::pair<ProcessorReturnCode, std::vector<std::string>> all_program_names() const override;\n\n    ProcessorReturnCode set_program(int program) override;\n\n    ProcessorReturnCode set_state(ProcessorState* state, bool realtime_running) override;\n\n    ProcessorState save_state() const override;\n\n    PluginInfo info() const override;\n\n    static int worker_callback(void* data, EventId id)\n    {\n        reinterpret_cast<LV2_Wrapper*>(data)->_worker_callback(id);\n        return 1;\n    }\n\n    static int restore_state_callback(void* data, EventId id)\n    {\n        reinterpret_cast<LV2_Wrapper *>(data)->_restore_state_callback(id);\n        return 1;\n    }\n\n    void request_worker_callback(AsyncWorkCallback callback);\n\nprivate:\n    const LilvPlugin* _plugin_handle_from_URI(const std::string& plugin_URI_string);\n\n    void _worker_callback(EventId);\n\n    void _restore_state_callback(EventId);\n\n    void _update_transport();\n    uint8_t pos_buf[256];\n    LV2_Atom* _lv2_pos{nullptr};\n    bool _xport_changed{false};\n\n    /**\n     * @brief Iterate over LV2 parameters and register internal FloatParameterDescriptor\n     *        for each one of them.\n     * @return True if all parameters were registered properly.\n     */\n    bool _register_parameters();\n\n    void _fetch_plugin_name_and_label();\n\n    void _map_audio_buffers(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer);\n\n    void _deliver_inputs_to_plugin();\n    void _deliver_outputs_from_plugin(bool send_ui_updates);\n\n    bool _calculate_control_output_trigger();\n\n    static MidiDataByte _convert_event_to_midi_buffer(RtEvent& event);\n    void _flush_event_queue();\n    void _process_midi_input(Port* port);\n    void _process_midi_output(Port* port);\n\n    void _set_binary_state(ProcessorState* state);\n\n    float* _process_inputs[LV2_WRAPPER_MAX_N_CHANNELS]{};\n    float* _process_outputs[LV2_WRAPPER_MAX_N_CHANNELS]{};\n\n    ChunkSampleBuffer _dummy_input{1};\n\n    ChunkSampleBuffer _dummy_output{1};\n\n    std::string _plugin_path;\n\n    std::shared_ptr<LilvWorldWrapper> _world;\n\n    BypassManager _bypass_manager{_bypassed};\n\n    // This queue holds incoming midi events.\n    // They are parsed and converted to lv2_evbuf content for LV2 in\n    // process_audio(...).\n    RtSafeRtEventFifo _incoming_event_queue;\n\n    std::unique_ptr<Model> _model {nullptr};\n\n    // These are not used for other than the Unit tests,\n    // to simulate how the wrapper behaves if multithreaded.\n    PlayState _previous_play_state {PlayState::PAUSED};\n    void _pause_audio_processing();\n    void _resume_audio_processing();\n\n    // TODO: These are duplicated in ParameterPreProcessor, used for internal plugins.\n    // Eventually LV2 can instead use the same parameter processing subsystem:\n    // It has a field units:unit for instantiating an appropriate PreProcessor.\n    static float _to_domain(float value_normalized, float min_domain, float max_domain)\n    {\n        return max_domain + (min_domain - max_domain) / (_min_normalized - _max_normalized) * (value_normalized - _max_normalized);\n    }\n\n    static float _to_normalized(float value, float min_domain, float max_domain)\n    {\n        return _max_normalized + (_min_normalized - _max_normalized) / (min_domain - max_domain) * (value - max_domain);\n    }\n\n    static constexpr float _min_normalized{0.0f};\n    static constexpr float _max_normalized{1.0f};\n\n    std::map<ObjectId, const ParameterDescriptor*> _parameters_by_lv2_id;\n\n    int _control_output_refresh_interval{0};\n    int _control_output_sample_count{0};\n};\n\n} // end namespace sushi::internal::lv2\n\n#endif // SUSHI_LV2_PLUGIN_H\n"
  },
  {
    "path": "src/library/midi_decoder.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utility functions for decoding raw midi data.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cstdint>\n\n#include \"library/midi_decoder.h\"\n\nnamespace sushi::internal::midi {\n\nconstexpr uint8_t NOTE_OFF_PREFIX   = 0b1000;\nconstexpr uint8_t NOTE_ON_PREFIX    = 0b1001;\nconstexpr uint8_t POLY_PRES_PREFIX  = 0b1010;\nconstexpr uint8_t CTRL_CH_PREFIX    = 0b1011;\nconstexpr uint8_t PROG_CH_PREFIX    = 0b1100;\nconstexpr uint8_t CHAN_PRES_PREFIX  = 0b1101;\nconstexpr uint8_t PITCH_B_PREFIX    = 0b1110;\nconstexpr uint8_t SYSTEM_PREFIX     = 0b1111;\n\nconstexpr uint8_t SYSTEM_EX_CODE    = 0b0000;\nconstexpr uint8_t TIME_CODE         = 0b0001;\nconstexpr uint8_t SONG_POS_CODE     = 0b0010;\nconstexpr uint8_t SONG_SEL_CODE     = 0b0011;\nconstexpr uint8_t TUNE_REQ_CODE     = 0b0110;\nconstexpr uint8_t END_SYSEX_CODE    = 0b0111;\n\nconstexpr uint8_t TIMING_CLOCK_CODE = 0b0000;\nconstexpr uint8_t START_CMD_CODE    = 0b0010;\nconstexpr uint8_t CONTINUE_CMD_CODE = 0b0011;\nconstexpr uint8_t STOP_CMD_CODE     = 0b0100;\nconstexpr uint8_t ACTIVE_SENSING    = 0b0110;\nconstexpr uint8_t RESET_CODE        = 0b0111;\n\nconstexpr uint8_t SOUND_OFF_CTRL    = 120;\nconstexpr uint8_t RESET_CTRL        = 121;\nconstexpr uint8_t LOCAL_CTRL        = 122;\nconstexpr uint8_t NOTES_OFF_CTRL    = 123;\nconstexpr uint8_t OMNI_OFF_CTRL     = 124;\nconstexpr uint8_t OMNI_ON_CTRL      = 125;\nconstexpr uint8_t MONO_MODE_CTRL    = 126;\nconstexpr uint8_t POLY_MODE_CTRL    = 127;\n\nconstexpr uint8_t STRIP_MSG_BIT     = 0x7Fu;\nconstexpr uint8_t STRIP_4_MSG_BITS  = 0x0Fu;\nconstexpr uint8_t STRIP_4_LSG_BITS  = 0xF7u;\nconstexpr uint8_t STRIP_5_MSG_BITS  = 0x07u;\n\ninline bool realtime_msg(MidiDataByte data)\n{\n    return (data[0] & 0b00001000u) > 0;\n}\n\nMessageType decode_common_messages(MidiDataByte data)\n{\n    uint8_t last_3_bits = data[0] & STRIP_5_MSG_BITS;\n    switch (last_3_bits)\n    {\n        case SYSTEM_EX_CODE:\n            return MessageType::SYSTEM_EXCLUSIVE;\n\n        case TIME_CODE:\n            return MessageType::TIME_CODE;\n\n        case SONG_POS_CODE:\n            return MessageType::SONG_POSITION;\n\n        case SONG_SEL_CODE:\n            return MessageType::SONG_SELECT;\n\n        case TUNE_REQ_CODE:\n            return MessageType::TUNE_REQUEST;\n\n        case END_SYSEX_CODE:\n            return MessageType::END_OF_EXCLUSIVE;\n\n        default:\n            break;\n    }\n    return MessageType::UNKNOWN;\n}\n\n\nMessageType decode_realtime_message(MidiDataByte data)\n{\n    uint8_t last_3_bits = data[0] & STRIP_5_MSG_BITS;\n    switch(last_3_bits)\n    {\n        case TIMING_CLOCK_CODE:\n            return MessageType::TIMING_CLOCK;\n\n        case START_CMD_CODE:\n            return MessageType::START;\n\n        case CONTINUE_CMD_CODE:\n            return MessageType::CONTINUE;\n\n        case STOP_CMD_CODE:\n            return MessageType::STOP;\n\n        case ACTIVE_SENSING:\n            return MessageType::ACTIVE_SENSING;\n\n        case RESET_CODE:\n            return MessageType::RESET;\n\n        default:\n            return MessageType::UNKNOWN;\n    }\n}\n\n\nMessageType decode_control_change_type(MidiDataByte data)\n{\n    uint8_t controller_no = data[1] & STRIP_MSG_BIT;\n    if (controller_no <= MAX_CONTROLLER_NO)\n    {\n        return MessageType::CONTROL_CHANGE;\n    }\n    switch (controller_no)\n    {\n        case SOUND_OFF_CTRL:\n            return MessageType::ALL_SOUND_OFF;\n\n        case RESET_CTRL:\n            return MessageType::RESET_ALL_CONTROLLERS;\n\n        case LOCAL_CTRL:\n        {\n            uint8_t value = data[2] & STRIP_MSG_BIT;\n            switch(value)\n            {\n                case 0:\n                    return MessageType::LOCAL_CONTROL_OFF;\n\n                case 127:\n                    return MessageType::LOCAL_CONTROL_ON;\n\n                default:\n                    return MessageType::UNKNOWN;\n            }\n        }\n\n        case NOTES_OFF_CTRL:\n            return MessageType::ALL_NOTES_OFF;\n\n        case OMNI_OFF_CTRL:\n            return MessageType::OMNI_MODE_OFF;\n\n        case OMNI_ON_CTRL:\n            return MessageType::OMNI_MODE_ON;\n\n        case MONO_MODE_CTRL:\n            return MessageType::MONO_MODE_ON;\n\n        case POLY_MODE_CTRL:\n            return MessageType::POLY_MODE_ON;\n\n        default:\n            return MessageType::UNKNOWN;\n    }\n}\n\n\nMessageType decode_message_type(MidiDataByte data)\n{\n    uint8_t first_4_bits = data[0] >> 4u;\n    switch (first_4_bits)\n    {\n        case NOTE_OFF_PREFIX:\n            return MessageType::NOTE_OFF;\n\n        case NOTE_ON_PREFIX:\n            return MessageType::NOTE_ON;\n\n        case POLY_PRES_PREFIX:\n            return MessageType::POLY_KEY_PRESSURE;\n\n        case CTRL_CH_PREFIX:\n            return decode_control_change_type(data);\n\n        case PROG_CH_PREFIX:\n            return MessageType::PROGRAM_CHANGE;\n\n        case CHAN_PRES_PREFIX:\n            return MessageType::CHANNEL_PRESSURE;\n\n        case PITCH_B_PREFIX:\n            return MessageType::PITCH_BEND;\n\n        case SYSTEM_PREFIX:\n            if (realtime_msg(data))\n            {\n                return decode_realtime_message(data);\n            }\n            else\n            {\n                return decode_common_messages(data);\n            }\n\n        default:\n            return MessageType::UNKNOWN;\n    }\n}\n\n\nNoteOffMessage decode_note_off(MidiDataByte data)\n{\n    NoteOffMessage message;\n    message.channel = decode_channel(data);\n    message.note = data[1] & STRIP_MSG_BIT;\n    message.velocity = data[2] & STRIP_MSG_BIT;\n    return message;\n}\n\nNoteOnMessage decode_note_on(MidiDataByte data)\n{\n    NoteOnMessage message;\n    message.channel = decode_channel(data);\n    message.note = data[1] & STRIP_MSG_BIT;\n    message.velocity = data[2] & STRIP_MSG_BIT;\n    return message;\n}\n\nPolyKeyPressureMessage decode_poly_key_pressure(MidiDataByte data)\n{\n    PolyKeyPressureMessage message;\n    message.channel = decode_channel(data);\n    message.note = data[1] & STRIP_MSG_BIT;\n    message.pressure = data[2] & STRIP_MSG_BIT;\n    return message;\n}\n\nControlChangeMessage decode_control_change(MidiDataByte data)\n{\n    ControlChangeMessage message;\n    message.channel = decode_channel(data);\n    message.controller = data[1] & STRIP_MSG_BIT;\n    message.value = data[2] & STRIP_MSG_BIT;\n    return message;\n}\n\nProgramChangeMessage decode_program_change(MidiDataByte data)\n{\n    ProgramChangeMessage message;\n    message.channel = decode_channel(data);\n    message.program = data[1] & STRIP_MSG_BIT;\n    return message;\n}\n\nChannelPressureMessage decode_channel_pressure(MidiDataByte data)\n{\n    ChannelPressureMessage message;\n    message.channel = decode_channel(data);\n    message.pressure = data[1] & STRIP_MSG_BIT;\n    return message;\n}\n\nPitchBendMessage decode_pitch_bend(MidiDataByte data)\n{\n    PitchBendMessage message;\n    message.channel = decode_channel(data);\n    message.value = (data[1] & STRIP_MSG_BIT) + ((data[2] & STRIP_MSG_BIT) << 7);\n    return message;\n}\n\nTimeCodeMessage decode_time_code(MidiDataByte data)\n{\n    TimeCodeMessage message;\n    message.message_type = (data[1] & (STRIP_MSG_BIT & STRIP_4_LSG_BITS)) >> 4;\n    message.value = data[1] & STRIP_4_MSG_BITS;\n    return message;\n}\n\nSongPositionMessage decode_song_position(MidiDataByte data)\n{\n    SongPositionMessage message;\n    message.beats = (data[1] & STRIP_MSG_BIT) + ((data[2] & STRIP_MSG_BIT) << 7);\n    return message;\n}\n\nSongSelectMessage decode_song_select(MidiDataByte data)\n{\n    SongSelectMessage message;\n    message.index = data[1] & STRIP_MSG_BIT;\n    return message;\n}\n\n} // end namespace sushi::internal::midi"
  },
  {
    "path": "src/library/midi_decoder.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utility functions for decoding raw midi data.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n *\n *        To decode raw midi messages, first call decode_message_type\n *        which returns the type and checks that the length is correct\n *        for the type. Then you can safely call the corresponding\n *        decode function for that type of message.\n */\n\n#ifndef SUSHI_MIDI_DECODER_H\n#define SUSHI_MIDI_DECODER_H\n\n#include <cassert>\n\n#include \"sushi/types.h\"\n\nnamespace sushi::internal::midi {\n\n/* Max value for midi velocity, pressure, controller value, etc. */\nconstexpr int MAX_VALUE = 127;\nconstexpr float MAX_VALUE_F = MAX_VALUE;\n/* Max value for midi pitch bend (14 bit value). */\nconstexpr int MAX_PITCH_BEND = 16384;\n/* Middle value for pitch bend */\nconstexpr int PITCH_BEND_MIDDLE = 8192;\n/* Maximum controller number for cc messages */\nconstexpr int MAX_CONTROLLER_NO = 119;\n/* Modulation wheel controller number */\nconstexpr int MOD_WHEEL_CONTROLLER_NO = 1;\n\n/**\n * @brief Convert midi data passed in C-array style to internal representation\n * @param data pointer to array of midi bytes\n * @param size number of bytes to process\n * @return A MidiDataByte object encapsulating the midi data\n */\ninline MidiDataByte to_midi_data_byte(const uint8_t* data, int size)\n{\n    MidiDataByte data_byte{0};\n    for (int i = 0; i < std::max(size, static_cast<int>(data_byte.size())); ++i)\n    {\n        data_byte[i] = data[i];\n    }\n    return data_byte;\n}\n\n/**\n * @brief Enum to enable OMNI (all channels) as an option.\n */\nenum MidiChannel\n{\n    CH_1 = 0,\n    CH_2,\n    CH_3,\n    CH_4,\n    CH_5,\n    CH_6,\n    CH_7,\n    CH_8,\n    CH_9,\n    CH_10,\n    CH_11,\n    CH_12,\n    CH_13,\n    CH_14,\n    CH_15,\n    CH_16,\n    OMNI\n};\n\n/**\n * @brief Enum to represent a midi message type.\n */\nenum class MessageType\n{\n    /* Channel voice messages */\n    NOTE_OFF,\n    NOTE_ON,\n    POLY_KEY_PRESSURE,\n    CONTROL_CHANGE,\n    PROGRAM_CHANGE,\n    CHANNEL_PRESSURE,\n    PITCH_BEND,\n    /* Channel mode messages */\n    ALL_SOUND_OFF,\n    RESET_ALL_CONTROLLERS,\n    LOCAL_CONTROL_ON,\n    LOCAL_CONTROL_OFF,\n    ALL_NOTES_OFF,\n    OMNI_MODE_OFF,\n    OMNI_MODE_ON,\n    MONO_MODE_ON,\n    POLY_MODE_ON,\n    /* System common messages */\n    SYSTEM_EXCLUSIVE,\n    TIME_CODE,\n    SONG_POSITION,\n    SONG_SELECT,\n    TUNE_REQUEST,\n    END_OF_EXCLUSIVE,\n    /* System real time messages */\n    TIMING_CLOCK,\n    START,\n    CONTINUE,\n    STOP,\n    ACTIVE_SENSING,\n    RESET,\n    /* Unhandled or corrupt messages */\n    UNKNOWN\n};\n\nstruct NoteOffMessage\n{\n    uint8_t channel;\n    uint8_t note;\n    uint8_t velocity;\n};\n\nstruct NoteOnMessage\n{\n    uint8_t channel;\n    uint8_t note;\n    uint8_t velocity;\n};\n\nstruct PolyKeyPressureMessage\n{\n    uint8_t channel;\n    uint8_t note;\n    uint8_t pressure;\n};\n\nstruct ControlChangeMessage\n{\n    uint8_t channel;\n    uint8_t controller;\n    uint8_t value;\n};\n\nstruct ProgramChangeMessage\n{\n    uint8_t channel;\n    uint8_t program;\n};\n\nstruct ChannelPressureMessage\n{\n    uint8_t channel;\n    uint8_t pressure;\n};\n\nstruct PitchBendMessage\n{\n    uint8_t channel;\n    uint16_t value;\n};\n\nstruct TimeCodeMessage\n{\n    uint8_t message_type;\n    uint8_t value;\n};\n\nstruct SongPositionMessage\n{\n    uint16_t beats; /* No of beats since start of song, 1 beat = 6 midi clock ticks.*/\n};\n\nstruct SongSelectMessage\n{\n    uint8_t index;\n};\n\n/**\n * @brief Decode the type of midi message from a byte string\n * @param data Midi data.\n * @param size Length of midi data.\n * @return The decoded type of data.\n */\nMessageType decode_message_type(MidiDataByte data);\n\n/**\n * @brief Decode the channel number of a channel mode message.\n *        I.e. when the decoded message type is between\n *        ALL_SOUND_OFF and POLY_MODE_ON.\n * @param data First byte of the midi message.\n * @return The decoded channel number (0-15) from the given message\n */\ninline uint8_t decode_channel(MidiDataByte data)\n{\n    return static_cast<uint8_t>(data[0] & 0x0Fu);\n}\n\n/**\n * @brief Decode a midi note off message.\n * @param data Message data.\n * @return Decoded message.\n */\nNoteOffMessage decode_note_off(MidiDataByte data);\n\n/**\n * @brief Decode a midi note on message.\n * @param data Message data.\n * @return Decoded message.\n */\nNoteOnMessage decode_note_on(MidiDataByte data);\n\n/**\n * @brief Decode a midi control change message.\n * @param data Message data.\n * @return Decoded message.\n */\nControlChangeMessage decode_control_change(MidiDataByte data);\n\n/**\n * @brief Decode a midi polyphonic key pressure (aftertouch) message.\n * @param data Message data.\n * @return Decoded message.\n */\nPolyKeyPressureMessage decode_poly_key_pressure(MidiDataByte data);\n\n/**\n * @brief Decode a midi program change message.\n * @param data Message data.\n * @return Decoded message.\n */\nProgramChangeMessage decode_program_change(MidiDataByte data);\n\n/**\n * @brief Decode a midi channel pressure (non-polyphonic aftertouch) message.\n * @param data Message data.\n * @return Decoded message.\n */\nChannelPressureMessage decode_channel_pressure(MidiDataByte data);\n\n/**\n * @brief Decode a midi pitch bend message.\n * @param data Message data.\n * @return Decoded message.\n */\nPitchBendMessage decode_pitch_bend(MidiDataByte data);\n\n/**\n * @brief Decode a Midi time code quarter frame message.\n * @param data  Message data.\n * @return Decoded message.\n */\nTimeCodeMessage decode_time_code(MidiDataByte data);\n\n/**\n * @brief Decode a Midi song position message.\n * @param data  Message data.\n * @return Decoded message.\n */\nSongPositionMessage decode_song_position(MidiDataByte data);\n\n/**\n * @brief Decode a Midi song select message.\n * @param data  Message data.\n * @return Decoded message.\n */\nSongSelectMessage decode_song_select(MidiDataByte data);\n\n\n} // end namespace sushi::internal::midi\n\n\n#endif // SUSHI_MIDI_DECODER_H\n"
  },
  {
    "path": "src/library/midi_encoder.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utility functions for encoding raw midi data.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cmath>\n\n#include \"library/midi_encoder.h\"\n#include \"library/midi_decoder.h\"\n\nnamespace sushi::internal::midi {\n\n// Channel messages\nconstexpr uint8_t NOTE_OFF_PREFIX   = 0b10000000;\nconstexpr uint8_t NOTE_ON_PREFIX    = 0b10010000;\nconstexpr uint8_t POLY_PRES_PREFIX  = 0b10100000;\nconstexpr uint8_t CTRL_CHANGE_PREFIX= 0b10110000;\nconstexpr uint8_t CHAN_PRES_PREFIX  = 0b11010000;\nconstexpr uint8_t PITCH_BEND_PREFIX = 0b11100000;\nconstexpr uint8_t PGM_CHANGE_PREFIX = 0b11000000;\n\n// System real time messages\nconstexpr uint8_t TIMING_CLOCK_PREFIX   = 0b11111000;\nconstexpr uint8_t START_PREFIX          = 0b11111010;\nconstexpr uint8_t CONTINUE_PREFIX       = 0b11111011;\nconstexpr uint8_t STOP_PREFIX           = 0b11111100;\nconstexpr uint8_t ACTIVE_SENSING_PREFIX = 0b11111110;\nconstexpr uint8_t RESET_PREFIX          = 0b11111111;\n\n\nMidiDataByte encode_note_on(int channel, int note, float velocity)\n{\n    MidiDataByte data;\n    data[0] = NOTE_ON_PREFIX | static_cast<uint8_t>(channel);\n    data[1] = static_cast<uint8_t>(note);\n    data[2] = static_cast<uint8_t>(std::round(velocity * MAX_VALUE));\n    data[3] = 0;\n    return data;\n}\n\nMidiDataByte encode_note_off(int channel, int note, float velocity)\n{\n    MidiDataByte data;\n    data[0] = NOTE_OFF_PREFIX | static_cast<uint8_t>(channel);\n    data[1] = static_cast<uint8_t>(note);\n    data[2] = static_cast<uint8_t>(std::round(velocity * MAX_VALUE));\n    data[3] = 0;\n    return data;\n}\n\nMidiDataByte encode_poly_key_pressure(int channel, int note, float pressure)\n{\n    MidiDataByte data;\n    data[0] = POLY_PRES_PREFIX | static_cast<uint8_t>(channel);\n    data[1] = static_cast<uint8_t>(note);\n    data[2] = static_cast<uint8_t>(std::round(pressure * MAX_VALUE));\n    data[3] = 0;\n    return data;\n}\n\nMidiDataByte encode_control_change(int channel, int controller, float value)\n{\n    MidiDataByte data;\n    data[0] = CTRL_CHANGE_PREFIX | static_cast<uint8_t>(channel);\n    data[1] = static_cast<uint8_t>(controller);\n    data[2] = static_cast<uint8_t>(std::round(value * MAX_VALUE));\n    data[3] = 0;\n    return data;\n}\n\nMidiDataByte encode_channel_pressure(int channel, float value)\n{\n    MidiDataByte data;\n    data[0] = CHAN_PRES_PREFIX | static_cast<uint8_t>(channel);\n    data[1] = static_cast<uint8_t>(std::round(value * MAX_VALUE));\n    data[2] = 0;\n    data[3] = 0;\n    return data;\n}\n\nMidiDataByte encode_pitch_bend(int channel, float value)\n{\n    MidiDataByte data;\n    data[0] = PITCH_BEND_PREFIX | static_cast<uint8_t>(channel);\n    int pb_val = static_cast<int>((value + 1.0f) * PITCH_BEND_MIDDLE);\n    data[1] = static_cast<uint8_t>(pb_val & 0x00FF);\n    data[2] = static_cast<uint8_t>( (pb_val & 0xFF00) >> 7 );\n    data[3] = 0;\n    return data;\n}\n\nMidiDataByte encode_program_change(int channel, int program)\n{\n    MidiDataByte data;\n    data[0] = PGM_CHANGE_PREFIX | static_cast<uint8_t>(channel);\n    data[1] = static_cast<uint8_t>(program);\n    data[2] = 0;\n    data[3] = 0;\n    return data;\n}\n\nMidiDataByte encode_start_message()\n{\n    return {START_PREFIX, 0, 0, 0};\n}\n\nMidiDataByte encode_stop_message()\n{\n    return {STOP_PREFIX, 0, 0, 0};\n}\n\nMidiDataByte encode_continue_message()\n{\n    return {CONTINUE_PREFIX, 0, 0, 0};\n}\n\nMidiDataByte encode_timing_clock()\n{\n    return {TIMING_CLOCK_PREFIX, 0, 0, 0};\n}\n\nMidiDataByte encode_active_sensing()\n{\n    return {ACTIVE_SENSING_PREFIX, 0, 0, 0};\n}\n\nMidiDataByte encode_reset_message()\n{\n    return {RESET_PREFIX, 0, 0, 0};\n}\n\n\n} // end namespace sushi::internal::midi\n"
  },
  {
    "path": "src/library/midi_encoder.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utility functions for encoding raw midi data.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_MIDI_ENCODER_H\n#define SUSHI_MIDI_ENCODER_H\n\n#include \"sushi/types.h\"\n\nnamespace sushi::internal::midi {\n\n/**\n * @brief Encode a midi note on message\n * @param channel Midi channel to use (0-15)\n * @param note Midi note number\n * @param velocity Velocity (0-1)\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_note_on(int channel, int note, float velocity);\n\n/**\n * @brief Encode a midi note off message\n * @param channel Midi channel to use (0-15)\n * @param note Midi note number\n * @param velocity Release velocity (0-1)\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_note_off(int channel, int note, float velocity);\n\n/**\n * @brief Encode a polyphonic key pressure message\n * @param channel Midi channel to use (0-15)\n * @param note Midi note number\n * @param pressure Pressure (0-1)\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_poly_key_pressure(int channel, int note, float pressure);\n\n/**\n * @brief Encode a control change message\n * @param channel Midi channel to use (0-15)\n * @param controller Midi controller number (0-119)\n * @param value Value to send (0-1)\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_control_change(int channel, int controller, float value);\n\n/**\n * @brief Encode a channel pressure (after touch) message\n * @param channel Midi channel to use (0-15)\n * @param pressure Pressure (0-1)\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_channel_pressure(int channel, float value);\n\n/**\n * @brief Encode a pitch bend message\n * @param channel Midi channel to use (0-15)\n * @param value Pitch bend value (-1 to 1  where 0 is the middle position)\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_pitch_bend(int channel, float value);\n\n/**\n * @brief Encode a program change message\n * @param channel Midi channel to use (0-15)\n * @param program MIDI program change from 0 to 127\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_program_change(int channel, int program);\n\n/**\n * @brief Encode a midi clock start message\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_start_message();\n\n/**\n * @brief Encode a midi clock stop message\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_stop_message();\n/**\n * @brief Encode a midi clock continue message\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_continue_message();\n\n/**\n * @brief Encode a midi clock tick message\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_timing_clock();\n\n/**\n * @brief Encode a midi active sensing message\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_active_sensing();\n\n/**\n * @brief Encode a midi global reset message\n * @return A MidiDataByte containing the encoded message\n */\nMidiDataByte encode_reset_message();\n\n\n} // end namespace sushi::internal::midi\n\n#endif // SUSHI_MIDI_ENCODER_H\n"
  },
  {
    "path": "src/library/parameter_dump.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utility functions for dumping plugins' parameter info\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"sushi/utils.h\"\n\n#include \"sushi/parameter_dump.h\"\n\n#include \"engine/controller/controller.h\"\n#include \"control_frontends/osc_utils.h\"\n\nnamespace sushi {\n\nrapidjson::Document generate_processor_parameter_document(sushi::control::SushiControl* engine_controller)\n{\n    rapidjson::Document document;\n    document.SetObject();\n    rapidjson::Document::AllocatorType& allocator = document.GetAllocator();\n    rapidjson::Value processors(rapidjson::kArrayType);\n    auto graph_controller = engine_controller->audio_graph_controller();\n    auto param_controller = engine_controller->parameter_controller();\n\n    for (auto& track : graph_controller->get_all_tracks())\n    {\n        for (auto& processor : graph_controller->get_track_processors(track.id).second)\n        {\n            rapidjson::Value processor_obj(rapidjson::kObjectType);\n            processor_obj.AddMember(rapidjson::Value(\"name\", allocator).Move(),\n                                    rapidjson::Value(processor.name.c_str(), allocator).Move(), allocator);\n\n            processor_obj.AddMember(rapidjson::Value(\"label\", allocator).Move(),\n                                    rapidjson::Value(processor.label.c_str(), allocator).Move(), allocator);\n\n            processor_obj.AddMember(rapidjson::Value(\"processor_id\", allocator).Move(),\n                                    rapidjson::Value(processor.id).Move(), allocator);\n\n            processor_obj.AddMember(rapidjson::Value(\"parent_track_id\", allocator).Move(),\n                                    rapidjson::Value(track.id).Move(), allocator);\n\n            rapidjson::Value parameters(rapidjson::kArrayType);\n\n            for (auto& parameter : param_controller->get_processor_parameters(processor.id).second)\n            {\n                rapidjson::Value parameter_obj(rapidjson::kObjectType);\n                parameter_obj.AddMember(rapidjson::Value(\"name\", allocator).Move(),\n                                        rapidjson::Value(parameter.name.c_str(), allocator).Move(), allocator);\n\n                parameter_obj.AddMember(rapidjson::Value(\"label\", allocator).Move(),\n                                        rapidjson::Value(parameter.label.c_str(), allocator).Move(), allocator);\n\n                std::string osc_path(\"/parameter/\" + sushi::internal::osc::make_safe_path(processor.name) + \"/\" +\n                                     sushi::internal::osc::make_safe_path(parameter.name));\n                parameter_obj.AddMember(rapidjson::Value(\"osc_path\", allocator).Move(),\n                                        rapidjson::Value(osc_path.c_str(), allocator).Move(), allocator);\n\n                parameter_obj.AddMember(rapidjson::Value(\"id\", allocator).Move(),\n                                        rapidjson::Value(parameter.id).Move(), allocator);\n\n                parameters.PushBack(parameter_obj.Move(),allocator);\n            }\n            processor_obj.AddMember(rapidjson::Value(\"parameters\", allocator).Move(), parameters.Move(), allocator);\n            processors.PushBack(processor_obj.Move(), allocator);\n        }\n    }\n\n    document.AddMember(rapidjson::Value(\"plugins\", allocator), processors.Move(), allocator);\n\n    return document;\n}\n\n} // end namespace sushi::internal\n\n"
  },
  {
    "path": "src/library/performance_timer.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief For measuring processing performance\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <vector>\n\n#include \"elklog/static_logger.h\"\n\n#include \"performance_timer.h\"\n\nnamespace sushi::internal::performance {\n\nconstexpr auto EVALUATION_INTERVAL = std::chrono::seconds(1);\nconstexpr double SEC_TO_NANOSEC = 1'000'000'000.0;\nconstexpr float AVERAGING_FACTOR = 0.5f;\n\nstd::vector<TimePoint> empty_vector;\n\nPerformanceTimer::~PerformanceTimer()\n{\n    if (_enabled.load() == true)\n    {\n        this->enable(false);\n    }\n}\n\nvoid PerformanceTimer::set_timing_period(TimePoint timing_period)\n{\n    _period = static_cast<float>(timing_period.count());\n}\n\nvoid PerformanceTimer::set_timing_period(float samplerate, int buffer_size)\n{\n    _period = static_cast<float>(static_cast<double>(buffer_size) / samplerate * SEC_TO_NANOSEC);\n}\n\nstd::optional<ProcessTimings> PerformanceTimer::timings_for_node(int id)\n{\n    std::unique_lock<std::mutex> lock(_timing_lock);\n    const auto& node = _timings.find(id);\n    if (node != _timings.end())\n    {\n        return node->second.timings;\n    }\n\n    return std::nullopt;\n}\n\nvoid PerformanceTimer::enable(bool enabled)\n{\n    if (enabled && _enabled == false)\n    {\n        _enabled = true;\n        _process_thread = std::thread(&PerformanceTimer::_worker, this);\n    }\n    else if (!enabled && _enabled == true)\n    {\n        _enabled = false;\n        if (_process_thread.joinable())\n        {\n            _process_thread.join();\n        }\n        // Run once to clear all records\n        _update_timings();\n    }\n}\n\nbool PerformanceTimer::enabled() const\n{\n    return _enabled;\n}\n\nstd::vector<int> PerformanceTimer::enabled_detailed_timings() const\n{\n    std::lock_guard<std::mutex> lock(_timing_lock);\n    std::vector<int> ids;\n    for (const auto& node : _detailed_timings)\n    {\n        ids.push_back(node.first);\n    }\n    return ids;\n}\n\nvoid PerformanceTimer::_worker()\n{\n    while(_enabled.load())\n    {\n        auto start_time = std::chrono::system_clock::now();\n        this->_update_timings();\n        std::this_thread::sleep_until(start_time + EVALUATION_INTERVAL);\n    }\n}\n\nvoid PerformanceTimer::_update_timings()\n{\n    std::map<int, std::vector<TimingLogPoint>> sorted_data;\n    TimingLogPoint log_point;\n\n    while (_entry_queue.pop(log_point))\n    {\n        sorted_data[log_point.id].push_back(log_point);\n    }\n\n    for (const auto& node : sorted_data)\n    {\n        int id = node.first;\n        std::lock_guard<std::mutex> lock(_timing_lock);\n\n        auto detailed_node = _detailed_timings.find(id);\n        if (detailed_node != _detailed_timings.end())\n        {\n            for (const auto& i : node.second)\n            {\n                detailed_node->second.push_back(i.delta_time);\n            }\n        }\n\n        const auto& timings = _timings[id];\n        auto new_timings = _calculate_timings(node.second);\n        _timings[id].timings = _merge_timings(timings.timings, new_timings);\n    }\n}\n\nProcessTimings PerformanceTimer::_calculate_timings(const std::vector<TimingLogPoint>& entries) const\n{\n    float min_value{100};\n    float max_value{0};\n    float sum{0.0f};\n\n    for (const auto& entry : entries)\n    {\n        float process_time = static_cast<float>(entry.delta_time.count()) / _period;\n        sum += process_time;\n        min_value = std::min(min_value, process_time);\n        max_value = std::max(max_value, process_time);\n    }\n\n    return {sum / entries.size(), min_value, max_value};\n}\n\nProcessTimings PerformanceTimer::_merge_timings(ProcessTimings prev_timings, ProcessTimings new_timings)\n{\n    if (prev_timings.avg_case == 0.0f)\n    {\n        prev_timings.avg_case = new_timings.avg_case;\n    }\n    else\n    {\n        prev_timings.avg_case = (1.0f - AVERAGING_FACTOR) * prev_timings.avg_case + AVERAGING_FACTOR * new_timings.avg_case;\n    }\n\n    prev_timings.min_case = std::min(prev_timings.min_case, new_timings.min_case);\n    prev_timings.max_case = std::max(prev_timings.max_case, new_timings.max_case);\n\n    return prev_timings;\n}\n\nbool PerformanceTimer::clear_timings_for_node(int id)\n{\n    std::lock_guard<std::mutex> lock(_timing_lock);\n    const auto& node = _timings.find(id);\n    if (node != _timings.end())\n    {\n        new (&node->second.timings) (ProcessTimings);\n        return true;\n    }\n\n    return false;\n}\n\nvoid PerformanceTimer::clear_all_timings()\n{\n    std::lock_guard<std::mutex> lock(_timing_lock);\n    for (auto& node : _timings)\n    {\n        new (&node.second.timings) (ProcessTimings);\n    }\n}\nvoid PerformanceTimer::enable_detailed_timings(int node_id, bool enabled)\n{\n    std::lock_guard<std::mutex> lock(_timing_lock);\n    if (enabled)\n    {\n        _detailed_timings[node_id] = {};\n    }\n    else\n    {\n        auto node = _detailed_timings.find(node_id);\n        _detailed_timings.erase(node);\n    }\n}\nconst std::vector<std::chrono::nanoseconds>& PerformanceTimer::detailed_timings_for_node(int id)\n{\n    std::lock_guard<std::mutex> lock(_timing_lock);\n    auto node = _detailed_timings.find(id);\n    if (node != _detailed_timings.end())\n    {\n        return node->second;\n    }\n    else\n    {\n        return empty_vector;\n    }\n}\n\n\n} // end namespace sushi::internal::performance\n"
  },
  {
    "path": "src/library/performance_timer.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief For measuring processing performance\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_PERFORMANCE_TIMER_H\n#define SUSHI_PERFORMANCE_TIMER_H\n\n#include <chrono>\n#include <atomic>\n#include <thread>\n#include <map>\n#include <mutex>\n#include <vector>\n\n#include \"fifo/circularfifo_memory_relaxed_aquire_release.h\"\n#include \"twine/twine.h\"\n\n#include \"sushi/constants.h\"\n\n#include \"base_performance_timer.h\"\n#include \"spinlock.h\"\n\nnamespace sushi::internal::performance {\n\nusing TimePoint = std::chrono::nanoseconds;\nconstexpr int MAX_LOG_ENTRIES = 20000;\n\nclass Accessor;\n\nclass PerformanceTimer : public BasePerformanceTimer\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(PerformanceTimer);\n\n    PerformanceTimer() = default;\n    ~PerformanceTimer() override;\n\n    /**\n     * @brief Set the period to use for timings\n     * @param timing_period The timing period in nanoseconds\n     */\n    void set_timing_period(TimePoint timing_period) override;\n\n    /**\n     * @brief Ser the period to use for timings implicitly\n     * @param samplerate The samplerate in Hz\n     * @param buffer_size The audio buffer size in samples\n     */\n    void set_timing_period(float samplerate, int buffer_size) override;\n\n    /**\n     * @brief Entry point for timing section\n     * @return A timestamp representing the start of the timing period\n     */\n    TimePoint start_timer()\n    {\n        if (_enabled)\n        {\n            return twine::current_rt_time();\n        }\n        return std::chrono::nanoseconds(0);\n    }\n\n    /**\n     * @brief Exit point for timing section.\n     * @param start_time A timestamp from a previous call to start_timer()\n     * @param node_id An integer id to identify timings from this node\n     */\n    void stop_timer(TimePoint start_time, int node_id)\n    {\n        if (_enabled)\n        {\n            TimingLogPoint tp{node_id, twine::current_rt_time() - start_time};\n            _entry_queue.push(tp);\n            // if queue is full, drop entries silently.\n        }\n    }\n\n    /**\n     * @brief Exit point for timing section. Safe to call concurrently from\n     *       several threads.\n     * @param start_time A timestamp from a previous call to start_timer()\n     * @param node_id An integer id to identify timings from this node\n     */\n    void stop_timer_rt_safe(TimePoint start_time, int node_id)\n    {\n        if (_enabled)\n        {\n            TimingLogPoint tp{node_id, twine::current_rt_time() - start_time};\n            _queue_lock.lock();\n            _entry_queue.push(tp);\n            _queue_lock.unlock();\n            // if queue is full, drop entries silently.\n        }\n    }\n\n    /**\n     * @brief Enable or disable timings\n     * @param enabled Enable timings if true, disable if false\n     */\n    void enable(bool enabled) override;\n\n    /**\n     * @brief Enable recording of every timing for a particular node\n     * @param node_id The node id to record\n     * @param enabled Enable timings if true, disable if false\n     */\n    void enable_detailed_timings(int node_id, bool enabled) override;\n\n    /**\n     * @brief Query the enabled state\n     * @return True if the timer is enabled, false otherwise\n     */\n    bool enabled() const override;\n\n    /**\n     * @brief List all enabled detailed timings\n     * @return A std::list of node ids for which detailed timing is enabled\n     */\n    std::vector<int> enabled_detailed_timings() const override;\n\n    /**\n     * @brief Get the recorded timings from a specific node\n     * @param id An integer id representing a timing node\n     * @return A ProcessTimings object populated if the node has any timing records. Empty otherwise\n     */\n    std::optional<ProcessTimings> timings_for_node(int id) override;\n\n    /**\n     * @brief Get all recorded timings from a specific node\n     * @param id An integer id representing a timing node\n     * @return A vector of all recorded timings if the node has any timing records. Empty otherwise\n     */\n    const std::vector<std::chrono::nanoseconds>& detailed_timings_for_node(int id) override;\n\n    /**\n     * @brief Clear the recorded timings for a particular node\n     * @param id An integer id representing a timing node\n     * @return true if the node was found, false otherwise\n     */\n    bool clear_timings_for_node(int id) override;\n\n    /**\n     * @brief Reset all recorded timings\n     */\n    void clear_all_timings() override;\n\nprotected:\n    struct TimingLogPoint\n    {\n        int id {0};\n        TimePoint delta_time;\n    };\n\n    struct TimingNode\n    {\n        int id {0};\n        ProcessTimings timings;\n    };\n\n    void _worker();\n    void _update_timings();\n\n    ProcessTimings _calculate_timings(const std::vector<TimingLogPoint>& entries) const;\n    static ProcessTimings _merge_timings(ProcessTimings prev_timings, ProcessTimings new_timings);\n\n    std::thread _process_thread;\n    float _period{0};\n    std::atomic_bool _enabled {false};\n\n    std::map<int, TimingNode> _timings;\n    std::map<int, std::vector<TimePoint>> _detailed_timings;\n    mutable std::mutex _timing_lock;\n    SpinLock _queue_lock;\n    alignas(ASSUMED_CACHE_LINE_SIZE) memory_relaxed_aquire_release::CircularFifo<TimingLogPoint, MAX_LOG_ENTRIES> _entry_queue;\n\nprivate:\n    friend Accessor;\n};\n\n} // end namespace sushi::internal::performance\n\n#endif // SUSHI_PERFORMANCE_TIMER_H\n"
  },
  {
    "path": "src/library/plugin_parameters.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Container classes for plugin parameters\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_PLUGIN_PARAMETERS_H\n#define SUSHI_PLUGIN_PARAMETERS_H\n\n#include <memory>\n#include <cmath>\n#include <string>\n#include <cassert>\n\n#include \"sushi/constants.h\"\n#include \"sushi/types.h\"\n\n#include \"library/id_generator.h\"\n\n\nnamespace sushi::internal {\n\nenum class ParameterType\n{\n    FLOAT,\n    INT,\n    BOOL,\n    STRING,\n    DATA,\n};\n\nenum class Direction\n{\n    AUTOMATABLE,\n    OUTPUT\n};\n\n/**\n * @brief Base class for describing plugin parameters\n */\nclass ParameterDescriptor\n{\npublic:\n    ParameterDescriptor(const std::string& name,\n                        const std::string& label,\n                        const std::string& unit,\n                        ParameterType type) : _label(label), _name(name), _unit(unit), _type(type) {}\n\n    virtual ~ParameterDescriptor() = default;\n\n    /**\n     * @brief Returns the enumerated type of the parameter\n     */\n    ParameterType type() const {return _type;}\n\n    /**\n     * @brief Returns the display name of the parameter, i.e. \"Oscillator pitch\"\n     */\n    const std::string& label() const {return _label;}\n\n    /**\n    * @brief Returns a unique identifier to the parameter i.e. \"oscillator_2_pitch\"\n    */\n    const std::string& name() const {return _name;}\n\n    /**\n    * @brief Returns the unit of the parameter i.e. \"dB\" or \"Hz\"\n    */\n    const std::string& unit() const {return _unit;}\n\n    /**\n     * @brief Returns a unique identifier for this parameter\n     */\n    ObjectId id() const {return _id;}\n\n    /**\n     * @brief Set a new id\n     */\n    void set_id(ObjectId id) {_id = id;}\n\n    /**\n     * @brief Whether the parameter is automatable or not.\n     * @return true if the parameter can be automated, false otherwise\n     */\n    virtual bool automatable() const {return true;}\n\n    /**\n     * @brief Returns the maximum value of the parameter\n     * @return A float with the maximum representation of the parameter\n     */\n    virtual float min_domain_value() const {return 0;}\n\n    /**\n     * @brief Returns the minimum value of the parameter\n     * @return A float with the minimum representation of the parameter\n     */\n    virtual float max_domain_value() const {return 1;}\n\nprotected:\n    std::string _label;\n    std::string _name;\n    std::string _unit;\n    ObjectId _id {0};\n    ParameterType _type;\n};\n\n\n/**\n * @brief Parameter preprocessor for scaling or non-linear mapping. This basic,\n * templated base class with no processing implemented.\n */\ntemplate<typename T>\nclass ParameterPreProcessor\n{\npublic:\n    ParameterPreProcessor(T min, T max): _min_domain_value(min), _max_domain_value(max) {}\n    virtual ~ParameterPreProcessor() = default;\n\n    virtual T process_to_plugin(T value)\n    {\n        return value;\n    }\n\n    virtual T process_from_plugin(T value)\n    {\n        return value;\n    }\n\n    virtual T to_domain(float value_normalized)\n    {\n        return static_cast<T>(static_cast<float>(_max_domain_value) +\n                              static_cast<float>(_min_domain_value - _max_domain_value) /\n                              (_min_normalized - _max_normalized) *\n                              (value_normalized - _max_normalized));\n    }\n\n    virtual float to_normalized(T value)\n    {\n        return _max_normalized +\n               (_min_normalized - _max_normalized) /\n               static_cast<float>(_min_domain_value - _max_domain_value) *\n               static_cast<float>(value - _max_domain_value);\n    }\n\nprotected:\n    T _min_domain_value;\n    T _max_domain_value;\n\n    static constexpr float _min_normalized {0.0f};\n    static constexpr float _max_normalized {1.0f};\n};\n\n/**\n * @brief Formatter used to format the parameter value to a string\n */\ntemplate<typename T>\nclass ParameterFormatPolicy\n{\nprotected:\n    std::string format(const T value) const {return std::to_string(value);}\n};\n\n/*\n * The format() function can then be specialized for types that need special handling.\n */\ntemplate <> inline std::string ParameterFormatPolicy<bool>::format(bool value) const\n{\n    return value? \"True\": \"False\";\n}\n\ntemplate <> inline std::string ParameterFormatPolicy<std::string*>::format(std::string* value) const\n{\n    return *value;\n}\n\ntemplate <> inline std::string ParameterFormatPolicy<BlobData>::format(BlobData /*value*/) const\n{\n    /* This parameter type is intended to transfer opaque binary data, and\n     * consequently there is no format policy that would work. */\n    return \"Binary data\";\n}\n\n/**\n * @brief Templated plugin parameter, works out of the box for native\n * types like float, int, etc. Needs specialization for more complex\n * types, for which the template type should likely be a pointer.\n */\ntemplate<typename T, ParameterType enumerated_type>\nclass TypedParameterDescriptor : public ParameterDescriptor\n{\npublic:\n    /**\n     * @brief Construct a parameter\n     */\n    TypedParameterDescriptor(const std::string& name,\n                             const std::string& label,\n                             const std::string& unit,\n                             const T min_domain_value,\n                             const T max_domain_Value,\n                             Direction automatable,\n                             ParameterPreProcessor<T>* pre_processor) :\n                                        ParameterDescriptor(name, label, unit, enumerated_type),\n                                        _pre_processor(pre_processor),\n                                        _min_domain_value(min_domain_value),\n                                        _max_domain_value(max_domain_Value)\n    {\n        if (automatable == Direction::AUTOMATABLE)\n        {\n            _automatable = true;\n        }\n        else\n        {\n            _automatable = false;\n        }\n    }\n\n    ~TypedParameterDescriptor() = default;\n\n    float min_domain_value() const override {return static_cast<float>(_min_domain_value);}\n    float max_domain_value() const override {return static_cast<float>(_max_domain_value);}\n\n    bool automatable() const override {return _automatable;}\n\nprivate:\n    std::unique_ptr<ParameterPreProcessor<T>> _pre_processor;\n    T _min_domain_value;\n    T _max_domain_value;\n\n    bool _automatable;\n};\n\n/* Partial specialization for pointer type parameters */\ntemplate<typename T, ParameterType enumerated_type>\nclass TypedParameterDescriptor<T *, enumerated_type> : public ParameterDescriptor\n{\npublic:\n    TypedParameterDescriptor(const std::string& name,\n                             const std::string& label,\n                             const std::string& unit,\n                             Direction automatable = Direction::AUTOMATABLE) :\n                                             ParameterDescriptor(name, label, unit, enumerated_type),\n                                             _automatable(automatable == Direction::AUTOMATABLE)\n    {}\n\n    ~TypedParameterDescriptor() = default;\n\n    bool automatable() const override {return _automatable;}\n\nprivate:\n    bool _automatable;\n};\n\n/* Partial specialization for pointer type parameters */\n//template<typename T, ParameterType enumerated_type>\ntemplate<>\nclass TypedParameterDescriptor<BlobData, ParameterType::DATA> : public ParameterDescriptor\n{\npublic:\n    TypedParameterDescriptor(const std::string& name,\n                             const std::string& label,\n                             const std::string& unit) : ParameterDescriptor(name, label, unit, ParameterType::DATA) {}\n\n    ~TypedParameterDescriptor() override = default;\n\n    bool automatable() const override {return false;}\n};\n\n/*\n * The templated forms are not intended to be accessed directly.\n * Instead, the below provide direct access to the right\n * type combinations.\n */\nusing FloatParameterPreProcessor = ParameterPreProcessor<float>;\nusing IntParameterPreProcessor = ParameterPreProcessor<int>;\nusing BoolParameterPreProcessor = ParameterPreProcessor<bool>;\n\nusing FloatParameterDescriptor = TypedParameterDescriptor<float, ParameterType::FLOAT>;\nusing IntParameterDescriptor = TypedParameterDescriptor<int, ParameterType::INT>;\nusing BoolParameterDescriptor = TypedParameterDescriptor<bool, ParameterType::BOOL>;\nusing StringPropertyDescriptor = TypedParameterDescriptor<std::string*, ParameterType::STRING>;\nusing DataPropertyDescriptor = TypedParameterDescriptor<BlobData, ParameterType::DATA>;\n\n/**\n * @brief Preprocessor example to map from decibels to linear gain.\n */\nclass dBToLinPreProcessor : public FloatParameterPreProcessor\n{\npublic:\n    dBToLinPreProcessor(float min, float max): FloatParameterPreProcessor(min, max) {}\n\n    float process_to_plugin(float value) override\n    {\n        return powf(10.0f, value / 20.0f);\n    }\n\n    float process_from_plugin(float value) override\n    {\n        return value;\n    }\n};\n\n/**\n * @brief Preprocessor example to map from linear gain to decibels.\n */\nclass LinTodBPreProcessor : public FloatParameterPreProcessor\n{\npublic:\n    LinTodBPreProcessor(float min, float max): FloatParameterPreProcessor(min, max) {}\n\n    float process_to_plugin(float value) override\n    {\n        return 20.0f * log10(value);\n    }\n\n    float process_from_plugin(float value) override\n    {\n        return value;\n    }\n};\n\n/**\n * @brief Preprocessor that applies a cubic map to the normalized parameter before scaling\n */\nclass CubicWarpPreProcessor : public FloatParameterPreProcessor\n{\npublic:\n    CubicWarpPreProcessor(float min, float max): FloatParameterPreProcessor(min, max) {}\n\n    float to_domain(float value_normalized) override\n    {\n        float x = value_normalized;\n        float y = x * x * x;\n        return _max_domain_value + (_min_domain_value - _max_domain_value) / (_min_normalized - _max_normalized) * (y - _max_normalized);\n    }\n\n    float to_normalized(float value) override\n    {\n        float y = _max_normalized + (_min_normalized - _max_normalized) / (_min_domain_value - _max_domain_value) * (value - _max_domain_value);\n        return powf(y, 1.0f / 3.0f);\n    }\n};\n\n\ntemplate<typename T, ParameterType enumerated_type>\nclass ParameterValue\n{\npublic:\n    ParameterValue(ParameterPreProcessor<T>* pre_processor,\n                   T value, ParameterDescriptor* descriptor) : _descriptor(descriptor),\n                                                               _pre_processor(pre_processor),\n                                                               _processed_value(pre_processor->process_to_plugin(value)),\n                                                               _normalized_value(pre_processor->to_normalized(value)) {}\n\n    [[nodiscard]] ParameterType type() const {return _type;}\n\n    [[nodiscard]] T processed_value() const {return _processed_value;}\n\n    [[nodiscard]] T domain_value() const {return _pre_processor->process_from_plugin(_pre_processor->to_domain(_normalized_value));}\n\n    [[nodiscard]] float normalized_value() const { return _normalized_value; }\n\n    [[nodiscard]] ParameterDescriptor* descriptor() const {return _descriptor;}\n\n    void set(float value_normalized)\n    {\n        _normalized_value = value_normalized;\n        _processed_value = _pre_processor->process_to_plugin(_pre_processor->to_domain(value_normalized));\n    }\n\n    void set_processed(float value_processed)\n    {\n        _processed_value = static_cast<T>(value_processed);\n        _normalized_value = _pre_processor->to_normalized(_pre_processor->process_from_plugin(static_cast<T>(value_processed)));\n    }\n\nprivate:\n    ParameterType _type {enumerated_type};\n    ParameterDescriptor* _descriptor {nullptr};\n    ParameterPreProcessor<T>* _pre_processor {nullptr};\n    T _processed_value;\n    float _normalized_value; // Always not processed, but raw as set from the outside.\n};\n\n/* Specialization for bool values, lack a pre_processor */\ntemplate<>\nclass ParameterValue<bool, ParameterType::BOOL>\n{\npublic:\n    ParameterValue(bool value, ParameterDescriptor* descriptor) : _descriptor(descriptor),\n                                                                  _processed_value(value) {}\n\n    ParameterType type() const {return _type;}\n\n    bool processed_value() const {return _processed_value;}\n\n    bool domain_value() const {return _processed_value;}\n\n    float normalized_value() const {return _processed_value? 1.0f : 0.0f;}\n\n    ParameterDescriptor* descriptor() const {return _descriptor;}\n\n    void set_values(bool value, bool raw_value) { _processed_value = value; _processed_value = raw_value;}\n    void set(bool value) { _processed_value = value;}\n\nprivate:\n    ParameterType _type{ParameterType::BOOL};\n    ParameterDescriptor* _descriptor{nullptr};\n    bool _processed_value;\n};\n\nusing BoolParameterValue = ParameterValue<bool, ParameterType::BOOL>;\nusing IntParameterValue = ParameterValue<int, ParameterType::INT>;\nusing FloatParameterValue = ParameterValue<float, ParameterType::FLOAT>;\n\nclass ParameterStorage\n{\npublic:\n    BoolParameterValue* bool_parameter_value()\n    {\n        assert(_bool_value.type() == ParameterType::BOOL);\n        return &_bool_value;\n    }\n\n    IntParameterValue* int_parameter_value()\n    {\n        assert(_int_value.type() == ParameterType::INT);\n        return &_int_value;\n    }\n\n    FloatParameterValue* float_parameter_value()\n    {\n        assert(_float_value.type() == ParameterType::FLOAT);\n        return &_float_value;\n    }\n\n    const BoolParameterValue* bool_parameter_value() const\n    {\n        assert(_bool_value.type() == ParameterType::BOOL);\n        return &_bool_value;\n    }\n\n    const IntParameterValue* int_parameter_value() const\n    {\n        assert(_int_value.type() == ParameterType::INT);\n        return &_int_value;\n    }\n\n    const FloatParameterValue* float_parameter_value() const\n    {\n        assert(_float_value.type() == ParameterType::FLOAT);\n        return &_float_value;\n    }\n\n    ParameterType type() const {return _float_value.type();}\n\n    ObjectId id() const {return _bool_value.descriptor()->id();}\n\n    /* Factory functions for construction */\n    static ParameterStorage make_bool_parameter_storage(ParameterDescriptor* descriptor,\n                                                        bool default_value)\n    {\n        BoolParameterValue value(default_value, descriptor);\n        return ParameterStorage(value);\n    }\n\n    static ParameterStorage make_int_parameter_storage(ParameterDescriptor* descriptor,\n                                                       int default_value,\n                                                       IntParameterPreProcessor* pre_processor)\n    {\n        IntParameterValue value(pre_processor, default_value, descriptor);\n        return ParameterStorage(value);\n    }\n\n    static ParameterStorage make_float_parameter_storage(ParameterDescriptor* descriptor,\n                                                         float default_value,\n                                                         FloatParameterPreProcessor* pre_processor)\n    {\n        FloatParameterValue value(pre_processor, default_value, descriptor);\n        return ParameterStorage(value);\n    }\n\nprivate:\n    explicit ParameterStorage(BoolParameterValue value) : _bool_value(value) {}\n    explicit ParameterStorage(IntParameterValue value) : _int_value(value) {}\n    explicit ParameterStorage(FloatParameterValue value) : _float_value(value) {}\n\n    union\n    {\n        BoolParameterValue  _bool_value;\n        IntParameterValue   _int_value;\n        FloatParameterValue _float_value;\n    };\n};\n\n/* We need this to be able to copy the ParameterValues by value into a container */\nstatic_assert(std::is_trivially_copyable<ParameterStorage>::value, \"\");\n\n}  // end namespace sushi\n\n#endif // SUSHI_PLUGIN_PARAMETERS_H\n"
  },
  {
    "path": "src/library/plugin_registry.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include \"plugin_registry.h\"\n\n#include \"internal_processor_factory.h\"\n#include \"vst2x/vst2x_processor_factory.h\"\n#include \"vst3x/vst3x_processor_factory.h\"\n#include \"lv2/lv2_processor_factory.h\"\n\nnamespace sushi::internal {\n\nstd::pair<ProcessorReturnCode, std::shared_ptr<Processor>>\nPluginRegistry::new_instance(const PluginInfo& plugin_info,\n                             HostControl& host_control,\n                             float sample_rate)\n{\n    if (_factories.count(plugin_info.type) == 0)\n    {\n        switch (plugin_info.type)\n        {\n            case PluginType::INTERNAL:\n            {\n                std::unique_ptr<BaseProcessorFactory> new_factory = std::make_unique<InternalProcessorFactory>();\n                _factories[plugin_info.type] = std::move(new_factory);\n                break;\n            }\n            case PluginType::VST2X:\n            {\n                std::unique_ptr<BaseProcessorFactory> new_factory = std::make_unique<vst2::Vst2xProcessorFactory>();\n                _factories[plugin_info.type] = std::move(new_factory);\n                break;\n            }\n            case PluginType::VST3X:\n            {\n                std::unique_ptr<BaseProcessorFactory> new_factory = std::make_unique<vst3::Vst3xProcessorFactory>();\n                _factories[plugin_info.type] = std::move(new_factory);\n                break;\n            }\n            case PluginType::LV2:\n            {\n                std::unique_ptr<BaseProcessorFactory> new_factory = std::make_unique<lv2::Lv2ProcessorFactory>();\n                _factories[plugin_info.type] = std::move(new_factory);\n                break;\n            }\n            default:\n                return {ProcessorReturnCode::PLUGIN_LOAD_ERROR, nullptr};\n        }\n    }\n\n    return _factories[plugin_info.type]->new_instance(plugin_info, host_control, sample_rate);\n}\n\n} // end namespace sushi::internal\n"
  },
  {
    "path": "src/library/plugin_registry.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Container and Facade for plugin factories.\n *        Currently, one plugin factory is instantiated and stored, per distinct PluginInfo.\n *        This means e.g. if you instantiate an LV2 metronome, and then an LV2 delay, there are 2 factories stored.\n */\n\n#ifndef SUSHI_PLUGIN_REGISTRY_H\n#define SUSHI_PLUGIN_REGISTRY_H\n\n#include <unordered_map>\n\n#include \"library/processor.h\"\n#include \"library/base_processor_factory.h\"\n#include \"engine/base_engine.h\"\n\nnamespace sushi::internal {\n\nstruct Hash\n{\n    size_t operator() (const PluginType &type) const\n    {\n        return std::hash<int>{}(static_cast<int>(type));\n    }\n};\n\nclass PluginRegistry\n{\npublic:\n    std::pair<ProcessorReturnCode, std::shared_ptr<Processor>> new_instance(const PluginInfo& plugin_info,\n                                                                            HostControl& host_control,\n                                                                            float sample_rate);\n\nprivate:\n    std::unordered_map<PluginType, std::unique_ptr<BaseProcessorFactory>, Hash> _factories;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_PLUGIN_REGISTRY_H\n"
  },
  {
    "path": "src/library/processor.cpp",
    "content": "#include <algorithm>\n\n#include \"elklog/static_logger.h\"\n\n#include \"processor.h\"\n\n#include \"library/midi_decoder.h\"\n\nnamespace sushi::internal {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"processor\");\n\nProcessor::~Processor()\n{\n    ELKLOG_LOG_INFO(\"Destroyed processor {}({})\", _id, _unique_name);\n}\n\nProcessorReturnCode Processor::connect_cv_from_parameter(ObjectId parameter_id, int cv_output_id)\n{\n    if (cv_output_id >= static_cast<int>(_cv_out_connections.size()) ||\n        _outgoing_cv_connections >= static_cast<int>(_cv_out_connections.size()))\n    {\n        return ProcessorReturnCode::ERROR;\n    }\n    bool param_exists = false;\n    for (const auto& p : this->all_parameters())\n    {\n        // Loop over all parameters since parameter ids don't necessarily match indexes (VST 3 for instance)\n        if (p->id() == parameter_id)\n        {\n            param_exists = true;\n            break;\n        }\n    }\n    if (param_exists == false)\n    {\n        return ProcessorReturnCode::PARAMETER_NOT_FOUND;\n    }\n    _cv_out_connections[_outgoing_cv_connections].parameter_id = parameter_id;\n    _cv_out_connections[_outgoing_cv_connections].cv_id = cv_output_id;\n    _outgoing_cv_connections++;\n    return ProcessorReturnCode::OK;\n}\n\nProcessorReturnCode Processor::connect_gate_from_processor(int gate_output_id, int channel, int note_no)\n{\n    assert(gate_output_id < MAX_ENGINE_GATE_PORTS || note_no <= MAX_ENGINE_GATE_NOTE_NO);\n    GateKey key = to_gate_key(static_cast<int8_t>(channel), static_cast<int8_t>(note_no));\n    if (_outgoing_gate_connections.count(key) > 0)\n    {\n        return ProcessorReturnCode::ERROR;\n    }\n    GateOutConnection con;\n    con.channel = static_cast<int8_t>(channel);\n    con.note = static_cast<int8_t>(note_no);\n    con.gate_id = gate_output_id;\n    _outgoing_gate_connections[key] = con;\n\n    return ProcessorReturnCode::OK;\n}\n\nbool Processor::register_parameter(ParameterDescriptor* parameter, ObjectId id)\n{\n    for (auto& p : _parameters_by_index)\n    {\n        if (p->id() == id) return false; // Don't allow duplicate parameter id:s\n    }\n    bool inserted = true;\n    std::tie(std::ignore, inserted) = _parameters.insert(std::pair<std::string, std::unique_ptr<ParameterDescriptor>>(parameter->name(), std::unique_ptr<ParameterDescriptor>(parameter)));\n    if (!inserted)\n    {\n        return false;\n    }\n    parameter->set_id(id);\n    _parameters_by_index.push_back(parameter);\n    return true;\n}\n\nbool Processor::maybe_output_cv_value(ObjectId parameter_id, float value)\n{\n    // Linear complexity lookup, though the number of outgoing connections are a handful at max\n    // and this is in memory which should already be cached, so very efficient\n    for (int i = 0; i < _outgoing_cv_connections; ++i)\n    {\n        auto& connection = _cv_out_connections[i];\n        if (parameter_id == connection.parameter_id)\n        {\n            output_event(RtEvent::make_cv_event(this->id(), 0, connection.cv_id, value));\n            return true;\n        }\n    }\n    return false;\n}\n\nbool Processor::maybe_output_gate_event(int channel, int note, bool note_on)\n{\n    auto con = _outgoing_gate_connections.find(to_gate_key(static_cast<int8_t>(channel),\n                                                           static_cast<int8_t>(note)));\n    if (con == _outgoing_gate_connections.end())\n    {\n        return false;\n    }\n    output_event(RtEvent::make_gate_event(this->id(), 0, con->second.gate_id, note_on));\n    return true;\n}\n\nvoid Processor::bypass_process(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer)\n{\n    if (_current_input_channels == 0)\n    {\n        out_buffer.clear();\n    }\n    else if (_current_input_channels == _current_output_channels)\n    {\n        out_buffer = in_buffer;\n    }\n    else\n    {\n        for (int c = 0; c < _current_output_channels; ++c)\n        {\n            out_buffer.replace(c, c % _current_input_channels, in_buffer);\n        }\n    }\n}\n\nvoid Processor::output_midi_event_as_internal(MidiDataByte midi_data, int sample_offset)\n{\n    auto msg_type = midi::decode_message_type(midi_data);\n    switch (msg_type)\n    {\n        case midi::MessageType::NOTE_ON:\n        {\n            auto msg = midi::decode_note_on(midi_data);\n            if (maybe_output_gate_event(msg.channel, msg.note, true) == false)\n            {\n                output_event(RtEvent::make_note_on_event(this->id(), sample_offset, msg));\n            }\n            break;\n        }\n        case midi::MessageType::NOTE_OFF:\n        {\n            auto msg = midi::decode_note_off(midi_data);\n            if (maybe_output_gate_event(msg.channel, msg.note, false) == false)\n            {\n                output_event(RtEvent::make_note_off_event(this->id(), sample_offset, msg));\n            }\n            break;\n        }\n        case midi::MessageType::PITCH_BEND:\n        {\n            auto msg = midi::decode_pitch_bend(midi_data);\n            output_event(RtEvent::make_pitch_bend_event(this->id(), sample_offset, msg));\n            break;\n        }\n        case midi::MessageType::CONTROL_CHANGE:\n        {\n            auto msg = midi::decode_control_change(midi_data);\n            if (msg.controller == midi::MOD_WHEEL_CONTROLLER_NO)\n            {\n                output_event(RtEvent::make_kb_modulation_event(this->id(), sample_offset, msg));\n            }\n            break;\n        }\n        case midi::MessageType::POLY_KEY_PRESSURE:\n        {\n            auto msg = midi::decode_poly_key_pressure(midi_data);\n            output_event(RtEvent::make_note_aftertouch_event(this->id(), sample_offset, msg));\n            break;\n        }\n        case midi::MessageType::CHANNEL_PRESSURE:\n        {\n            auto msg = midi::decode_channel_pressure(midi_data);\n            output_event(RtEvent::make_aftertouch_event(this->id(), sample_offset, msg));\n            break;\n        }\n        default:\n            break;\n\n    }\n}\n\nEventId Processor::request_non_rt_task(AsyncWorkCallback callback)\n{\n    auto event = RtEvent::make_async_work_event(callback, this->id(), this);\n    output_event(event);\n    return event.async_work_event()->event_id();\n}\n\nvoid Processor::async_delete(RtDeletable* object)\n{\n    auto rt_event = RtEvent::make_delete_data_event(object);\n    output_event(rt_event);\n}\n\nvoid Processor::notify_state_change_rt()\n{\n    auto rt_event = RtEvent::make_processor_notify_event(this->id(), ProcessorNotifyRtEvent::Action::PARAMETER_UPDATE);\n    output_event(rt_event);\n}\n\nstd::string Processor::_make_unique_parameter_name(const std::string& name) const\n{\n    auto unique_name = name.empty() ? \"parameter\" : name;\n    int index = 1;\n    while (this->parameter_from_name(unique_name) != nullptr)\n    {\n        unique_name = name + \"_\" + std::to_string(++index);\n    }\n    return unique_name;\n}\n\nvoid BypassManager::crossfade_output(const ChunkSampleBuffer& input_buffer, ChunkSampleBuffer& output_buffer,\n                                     int input_channels, int output_channels)\n{\n    auto [start, end] = get_ramp();\n    output_buffer.ramp(start, end);\n    if (input_channels > 0)\n    {\n        for (int c = 0; c < output_channels; ++c)\n        {\n            // Add the input with an inverse ramp to crossfade between input and output\n            output_buffer.add_with_ramp(c, c % input_channels, input_buffer, 1.0f - start, 1.0f - end);\n        }\n    }\n}\n\nstd::pair<float, float> BypassManager::get_ramp()\n{\n    int prev_count = 0;\n    if (_state == BypassState::RAMPING_DOWN)\n    {\n        prev_count = _ramp_count--;\n        if (_ramp_count == 0)\n        {\n            _state = BypassState::BYPASSED;\n        }\n    }\n    else if (_state == BypassState::RAMPING_UP)\n    {\n        prev_count = _ramp_count++;\n        if (_ramp_count == _ramp_chunks)\n        {\n            _state = BypassState::NOT_BYPASSED;\n        }\n    }\n    else\n    {\n        return {1.0f, 1.0f};\n    }\n    return {static_cast<float>(prev_count) / _ramp_chunks,\n            static_cast<float>(_ramp_count) / _ramp_chunks};\n}\n\n} // end namespace sushi::internal\n"
  },
  {
    "path": "src/library/processor.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Interface class for objects that process audio. Processor objects can be plugins,\n *        sends, faders, mixer/channel adaptors, or chains of processors.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_PROCESSOR_H\n#define SUSHI_PROCESSOR_H\n\n#include <map>\n#include <unordered_map>\n#include <vector>\n\n#include \"sushi/sample_buffer.h\"\n\n#include \"engine/host_control.h\"\n#include \"library/id_generator.h\"\n#include \"library/plugin_parameters.h\"\n#include \"library/rt_event.h\"\n#include \"library/rt_event_pipe.h\"\n#include \"processor_state.h\"\n\nnamespace sushi::internal {\n\nenum class ProcessorReturnCode\n{\n    OK,\n    ERROR,\n    PARAMETER_ERROR,\n    PARAMETER_NOT_FOUND,\n    MEMORY_ERROR,\n    UNSUPPORTED_OPERATION,\n    SHARED_LIBRARY_OPENING_ERROR,\n    PLUGIN_ENTRY_POINT_NOT_FOUND,\n    PLUGIN_LOAD_ERROR,\n    PLUGIN_INIT_ERROR,\n};\n\nenum class PluginType\n{\n    INTERNAL,\n    VST2X,\n    VST3X,\n    LV2\n};\n\n/**\n * @brief  Unique plugin descriptor, used to instantiate and identify a Plugin type throughout Sushi.\n */\nstruct PluginInfo\n{\n    std::string uid;\n    std::string path;\n    PluginType type;\n\n    bool operator == (const PluginInfo& other) const\n    {\n        return (uid == other.uid) &&\n               (path == other.path) &&\n               (type == other.type);\n    }\n};\n\nclass ProcessorAccessor;\n\nclass Processor\n{\npublic:\n    explicit Processor(HostControl host_control) : _host_control(host_control) {}\n\n    virtual ~Processor();\n\n    /**\n     * @brief Called by the host after instantiating the Processor, in a non-RT context. Most of the initialization, and\n     * all of the initialization that can fail, should be done here.\n     * any resources reserved here.\n     * @param sample_rate Host sample rate\n     */\n    virtual ProcessorReturnCode init(float /*sample_rate*/)\n    {\n        return ProcessorReturnCode::OK;\n    }\n\n    /**\n     * @brief Configure an already initialised plugin\n     * @param sample_rate the new sample rate to use\n     */\n    virtual void configure(float /*sample_rate*/) {}\n\n    /**\n     * @brief Process a single realtime event that is to take place during the next call to process\n     *        Called from an audio processing thread.\n     * @param event Event to process.\n     */\n    virtual void process_event(const RtEvent& event) = 0;\n\n    /**\n     * @brief Process a chunk of audio. Called from an audio processing thread.\n     * @param in_buffer Input SampleBuffer\n     * @param out_buffer Output SampleBuffer\n     */\n    virtual void process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer) = 0;\n\n    /**\n     * @brief Returns a unique name for this processor\n     * @return A string that uniquely identifies this processor\n     */\n    const std::string& name() const {return _unique_name;}\n\n    /**\n     * @brief Sets the unique name of the processor.\n     * @param name New Name\n     */\n    void set_name(const std::string& name) {_unique_name = name;}\n\n    /**\n     * @brief Returns display name for this processor\n     * @return Display name as a string\n     */\n    const std::string& label() const {return _label;}\n\n    /**\n     * @brief Sets the display name for this processor\n     * @param label New display name\n     */\n    void set_label(const std::string& label) {_label = label;}\n\n    /**\n     * @brief Returns a unique 32 bit identifier for this processor\n     * @return A unique 32 bit identifier\n     */\n    ObjectId id() const {return _id;}\n\n    /**\n     * @brief Set an output pipe for events.\n     * @param output_pipe the output EventPipe that should receive events\n     */\n    virtual void set_event_output(RtEventPipe* pipe)\n    {\n        _output_pipe = pipe;\n    }\n\n    /**\n     * @brief Get the number of parameters of this processor.\n     * @return The number of registered parameters for this processor.\n     */\n    int parameter_count() const {return static_cast<int>(_parameters_by_index.size());};\n\n    /**\n     * @brief Get the parameter descriptor associated with a certain name\n     * @param name The unique name of the parameter\n     * @return A pointer to the parameter descriptor or a null pointer\n     *         if there is no processor with that name\n     */\n    const ParameterDescriptor* parameter_from_name(const std::string& name) const\n    {\n        auto p = _parameters.find(name);\n        return (p != _parameters.end()) ? p->second.get() : nullptr;\n    }\n\n    /**\n     * @brief Get the parameter descriptor associated with a certain id\n     * @param id The id of the parameter\n     * @return A pointer to the parameter descriptor or a null pointer\n     *         if there is no processor with that id\n     */\n    virtual const ParameterDescriptor* parameter_from_id(ObjectId id) const\n    {\n        return (id < _parameters_by_index.size()) ? _parameters_by_index[id] : nullptr;\n    }\n\n    /**\n     * @brief Get all controllable parameters and properties of this processor\n     * @return A list of parameter objects\n     */\n    const std::vector<ParameterDescriptor*>& all_parameters() const\n    {\n        return _parameters_by_index;\n    }\n\n    int max_input_channels() const {return _max_input_channels;}\n    int max_output_channels() const {return _max_output_channels;}\n    int input_channels() const {return  _current_input_channels;}\n    int output_channels() const {return _current_output_channels;}\n\n    /**\n     * @brief Set the number of input audio channels of the Processor.\n     *        Must not be set to more channels than what is reported by\n     *        max_input_channels() or max_output_channels()\n     * @param inputs The new number of input channels\n     * @param outputs The new number of output channels\n     */\n    virtual void set_channels(int inputs, int outputs)\n    {\n        assert(inputs <= _max_input_channels);\n        assert(outputs <= _max_output_channels);\n\n        _current_input_channels = inputs;\n        _current_output_channels = outputs;\n    }\n\n    virtual bool enabled() {return _enabled;}\n\n    /* Override this for nested processors and set all sub processors to disabled */\n    /**\n     * @brief Set the processor to enabled or disabled. If disabled process_audio()\n     *        will not be called and the processor should clear any audio tails and\n     *        filter registers or anything else that could have an influence on future\n     *        calls to set_enabled(true).\n     * @param enabled New state of enabled.\n     */\n    virtual void set_enabled(bool enabled) {_enabled = enabled;}\n\n    virtual bool bypassed() const {return _bypassed;}\n\n    /**\n     * @brief Set the bypass state of the processor. If process_audio() is called\n     *        in bypass mode, the processor should pass the audio unchanged while\n     *        preserving any channel configuration set.\n     * @param bypassed New bypass state\n     */\n    virtual void set_bypassed(bool bypassed) {_bypassed = bypassed;}\n\n    /**\n     * @brief Get the value of the parameter with parameter_id, safe to call from\n     *        a non rt-thread\n     * @param parameter_id The Id of the requested parameter\n     * @return The current value of the parameter if the return code is OK\n     */\n    virtual std::pair<ProcessorReturnCode, float> parameter_value(ObjectId /*parameter_id*/) const\n    {\n        return {ProcessorReturnCode::PARAMETER_NOT_FOUND, 0.0f};\n    };\n\n    /**\n     * @brief Get the value of the  parameter with parameter_id, safe to call from\n     *        a non rt-thread\n     * @param parameter_id The Id of the requested parameter\n     * @return The current value normalised to a 0 to 1 range, if the return code is OK\n     */\n    virtual std::pair<ProcessorReturnCode, float> parameter_value_in_domain(ObjectId /*parameter_id*/) const\n    {\n        return {ProcessorReturnCode::PARAMETER_NOT_FOUND, 0.0f};\n    };\n\n    /**\n     * @brief Get the value of the parameter with parameter_id formatted as a string,\n     *        safe to call from a non rt-thread\n     * @param parameter_id The Id of the requested parameter\n     * @return The current value formatted as a string, if return code is OK\n     */\n    virtual std::pair<ProcessorReturnCode, std::string> parameter_value_formatted(ObjectId /*parameter_id*/) const\n    {\n        return {ProcessorReturnCode::PARAMETER_NOT_FOUND, \"\"};\n    };\n\n    /**\n     * @brief Get the value of a property. Should only be called from a non-rt thread\n     * @param property_id The id of the requested property\n     * @return A string containing the current string property value if return code is OK\n     */\n    virtual std::pair<ProcessorReturnCode, std::string> property_value(ObjectId /*property_id*/) const\n    {\n        return {ProcessorReturnCode::PARAMETER_NOT_FOUND, \"\"};\n    }\n\n    /**\n     * @brief Set the value of a property.  Should only be called from a non-rt thread\n     * @param property_id The id of the property to set\n     * @param value The new string value of the string property\n     * @return OK if the operation completed successfully\n     */\n    virtual ProcessorReturnCode set_property_value(ObjectId /*property_id*/, const std::string& /*value*/)\n    {\n        return ProcessorReturnCode::PARAMETER_NOT_FOUND;\n    }\n\n    /**\n     * @brief Whether or not the processor supports programs/presets\n     * @return True if the processor supports programs, false otherwise\n     */\n    virtual bool supports_programs() const {return false;}\n\n    /**\n     * @brief Get the number of stored programs in the processor\n     * @return The number of programs\n     */\n    virtual int program_count() const {return 0;}\n\n    /**\n     * @brief Get the currently active program\n     * @return The index of the current program\n     */\n    virtual int current_program() const {return 0;}\n\n    /**\n     * @brief Get the name of the currently active program\n     * @return The name of the current program\n     */\n    virtual std::string current_program_name() const {return \"\";}\n\n    /**\n     * @brief Get the name of a stored program\n     * @param program The index (starting from 0) of the requested program\n     * @return The name of the string if return is OK, otherwise the first member of the\n     *         pair contains an error code\n     */\n    virtual std::pair<ProcessorReturnCode, std::string> program_name(int /*program*/) const\n    {\n        return {ProcessorReturnCode::UNSUPPORTED_OPERATION, \"\"};\n    }\n\n    /**\n     * @brief Get all stored programs\n     * @return An std::vector with all program names and where the index in the vector\n     *         represents the program index, if the return is OK, otherwise the first\n     *         member of the pair contains an error code\n     */\n    virtual std::pair<ProcessorReturnCode, std::vector<std::string>> all_program_names() const\n    {\n        return {ProcessorReturnCode::UNSUPPORTED_OPERATION, std::vector<std::string>()};\n    }\n\n    /**\n     * @brief Set a new program to the processor. Called from a non-rt thread\n     * @param program The id of the new program to use\n     * @return OK if the operation was successful, error code otherwise\n     */\n    virtual ProcessorReturnCode set_program(int /*program*/) {return ProcessorReturnCode::UNSUPPORTED_OPERATION;}\n\n    /**\n     * @brief Connect a parameter of the processor to a cv out so that rt updates of\n     *        the parameter will be sent to the cv output\n     * @param parameter_id The id of the parameter to connect from\n     * @param cv_input_id The id of the cv output\n     * @return ProcessorReturnCode::OK on success, error code on failure\n     */\n    virtual ProcessorReturnCode connect_cv_from_parameter(ObjectId parameter_id, int cv_output_id);\n    /**\n     * @brief Connect note on and off events with a particular channel and note number\n     *        from this processor to a gate output.\n     * @param gate_output_id The gate output to output to.\n     * @param channel Only events with this channel will be routed.\n     * @param note_no The note number that will be used for this gate output.\n     * @return ProcessorReturnCode::OK on success, error code on failure\n     */\n    virtual ProcessorReturnCode connect_gate_from_processor(int gate_output_id, int channel, int note_no);\n\n    /**\n     * @brief Set the on Track status. Call with true when adding a Processor to a track or\n     *        track to the engine, and false when removing it.\n     * @param active True if Processor is being added to a Track, False otherwise.\n     * @param thread The audio processing thread that is currently calling process_audio\n     */\n    void set_active_rt_processing(bool active, int thread = 0)\n    {\n        _on_track = active;\n        _current_processing_thread = thread;\n    }\n\n    /**\n     * @brief Query whether the processor is currently active on a Track's processing chain\n     *        or if it's a Track, active in the engine.\n     * @return true if the Processor is on a track, false otherwise.\n     */\n    bool active_rt_processing() const\n    {\n        return _on_track;\n    }\n\n    /**\n     * @brief  Set the complete state of the Processor (bypass state, program, parameters)\n     *         according to the supplied state object.\n     * @param state The state object to apply to the Processor\n     * @param realtime_running If true the Processor needs to take realtime data access\n     *        concerns into account when applying the state.\n     * @return\n     */\n    virtual ProcessorReturnCode set_state(ProcessorState* /*state*/, bool /*realtime_running*/)\n    {\n        return ProcessorReturnCode::UNSUPPORTED_OPERATION;\n    }\n\n    virtual ProcessorState save_state() const\n    {\n        return {};\n    }\n\n    virtual PluginInfo info() const\n    {\n        return PluginInfo();\n    }\n\nprotected:\n    friend ProcessorAccessor;\n\n    /**\n     * @brief Register a newly created parameter\n     * @param parameter Pointer to a parameter object\n     * @return true if the parameter was successfully registered, false otherwise\n     */\n    bool register_parameter(ParameterDescriptor* parameter)\n    {\n        return register_parameter(parameter, static_cast<ObjectId>(_parameters_by_index.size()));\n    }\n\n    /**\n     * @brief Register a newly created parameter and manually set the id of the parameter\n     * @param parameter Pointer to a parameter object\n     * @param id The unique id to give to the parameter\n     * @return true if the parameter was successfully registered, false otherwise\n     */\n    bool register_parameter(ParameterDescriptor* parameter, ObjectId id);\n\n    /**\n     * @brief Convert midi data and output as an internal event, taking account any gate\n     *        routing configurations active on the processor.\n     * @param midi_data raw midi data from the plugin\n     * @param sample_offset Intra-buffer offset in samples\n     */\n    void output_midi_event_as_internal(MidiDataByte midi_data, int sample_offset);\n\n    void output_event(const RtEvent& event)\n    {\n        if (_output_pipe)\n            _output_pipe->send_event(event);\n    }\n\n    /**\n     * @brief Handle parameter updates if connected to cv outputs and send cv output event if\n     *        the parameter is connected to a cv output\n     * @param parameter_id The id of the parameter\n     * @param value The new value of the parameter change\n     * @return true If there is an active outgoing connection from this parameter, false otherwise\n     */\n    bool maybe_output_cv_value(ObjectId parameter_id, float value);\n\n    /**\n     * @brief Handle gate outputs if note on or note off events are mapped to gate outputs\n     * @param channel The channel id of the event\n     * @param note The midi note number of the event\n     * @param note_on True if this is a note on event, false if it is a note off event\n     * @return true if there is an active outgoing connection from this note/channel combination, false otherwise\n     */\n    bool maybe_output_gate_event(int channel, int note, bool note_on);\n\n    /**\n    * @brief Utility function do to general bypass/passthrough audio processing.\n    *        Useful for processors that don't implement this on their own.\n    * @param in_buffer Input SampleBuffer\n    * @param out_buffer Output SampleBuffer\n    */\n    void bypass_process(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer);\n\n    /**\n     * @brief Called from the audio callback to request work to be done in another,\n     *        non-realtime thread.\n     * @param callback The callback to call in the non realtime thread. The return\n     *        value from the callback will be communicated back to the plugin in the\n     *        form of an AsyncWorkRtCompletionEvent RtEvent.\n     * @return An EventId that can be used to identify the particular request.\n     */\n    EventId request_non_rt_task(AsyncWorkCallback callback);\n\n    /**\n     * @brief Called from a realtime thread to asynchronously delete an object outside the rt tread\n     * @param object The object to delete.\n     */\n    void async_delete(RtDeletable* object);\n\n    /**\n     * @brief Called from a realtime thread to notify that all parameter values have changed and\n     *        should be reloaded.\n     */\n    void notify_state_change_rt();\n\n    /**\n     * @brief Takes a parameter name and makes sure that it is unique and is not empty. An\n     *        index will be added in case of duplicates\n     * @param name The name of the parameter\n     * @return An std::string containing a unique parameter name\n     */\n    std::string _make_unique_parameter_name(const std::string& name) const;\n\n    /* Minimum number of output/input channels a processor should support should always be 0 */\n    int _max_input_channels{0};\n    int _max_output_channels{0};\n\n    int _current_input_channels{0};\n    int _current_output_channels{0};\n\n    int _current_processing_thread{0};\n\n    bool _enabled{false};\n    bool _bypassed{false};\n    bool _on_track{false};\n\n    HostControl _host_control;\n\nprivate:\n    RtEventPipe* _output_pipe{nullptr};\n    /* Automatically generated unique id for identifying this processor */\n    ObjectId _id{ProcessorIdGenerator::new_id()};\n\n    std::string _unique_name;\n    std::string _label;\n\n    std::map<std::string, std::unique_ptr<ParameterDescriptor>> _parameters;\n    std::vector<ParameterDescriptor*> _parameters_by_index;\n\n    struct CvOutConnection\n    {\n        ObjectId parameter_id;\n        int cv_id;\n    };\n\n    std::array<CvOutConnection, MAX_ENGINE_CV_IO_PORTS> _cv_out_connections;\n    int _outgoing_cv_connections{0};\n\n    using GateKey = int;\n    GateKey to_gate_key(int8_t channel, int8_t note)\n    {\n        return channel + (note << sizeof(note));\n    }\n    struct GateOutConnection\n    {\n        int8_t note;\n        int8_t channel;\n        int gate_id;\n    };\n    std::unordered_map<GateKey, GateOutConnection> _outgoing_gate_connections;\n};\n\n\n/**\n * @brief Convenience class to encapsulate the bypass state logic and when to ramp audio up\n *        or down in order to avoid audio clicks and artefacts.\n */\nclass BypassManager\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(BypassManager);\n\n    BypassManager() = default;\n    explicit BypassManager(bool bypassed_by_default) :\n            _state(bypassed_by_default? BypassState::BYPASSED : BypassState::NOT_BYPASSED) {}\n\n    explicit BypassManager(bool bypassed_by_default, std::chrono::milliseconds ramp_time) :\n            BypassManager(bypassed_by_default)\n    {\n        _ramp_time = ramp_time;\n    }\n\n    /**\n     * @brief Check whether or not bypass is enabled\n     * @return true if bypass is enabled\n     */\n    bool bypassed() const {return _state == BypassState::BYPASSED || _state == BypassState::RAMPING_DOWN;}\n\n    /**\n     * @brief Set the bypass state\n     * @param bypass_enabled If true, sets bypass enabled, if false, turns off bypass\n     * @param sample_rate The current sample rate, used for calculating ramp time\n     */\n    void set_bypass(bool bypass_enabled, float sample_rate)\n    {\n        if (bypass_enabled && this->bypassed() == false)\n        {\n            _state = BypassState::RAMPING_DOWN;\n            _ramp_chunks = chunks_to_ramp(sample_rate);\n            _ramp_count = _ramp_chunks;\n        }\n\n        if (bypass_enabled == false && this->bypassed())\n        {\n            _state = BypassState::RAMPING_UP;\n            _ramp_chunks = chunks_to_ramp(sample_rate);\n            _ramp_count = 0;\n        }\n    }\n\n    /**\n     * @return true if the processors processing functions needs to be called, false otherwise\n     */\n    [[nodiscard]] bool should_process() const {return _state != BypassState::BYPASSED;}\n\n    /**\n     * @return true if the processor output should be ramped, false if it doesn't need volume ramping\n     */\n    [[nodiscard]] bool should_ramp() const {return _state == BypassState::RAMPING_DOWN || _state == BypassState::RAMPING_UP;}\n\n    /**\n     * @brief Does volume ramping on the buffer passed to the function based on the current bypass state\n     * @param output_buffer The buffer to apply the volume ramp\n     */\n    void ramp_output(ChunkSampleBuffer& output_buffer)\n    {\n        auto [start, end] = get_ramp();\n        output_buffer.ramp(start, end);\n    }\n\n    /**\n     * @brief Does crossfade volume ramping between input and output buffer based in the\n     *        current bypass state.\n     * @param input_buffer The input buffer to the processor, what will be crossfaded to if\n     *                     bypass is enabled\n     * @param output_buffer A filled output buffer from the processor, what will be crossfaded\n     *                      to if bypass is disabled\n     * @param input_channels The current number of input channels of the processor\n     * @param output_channels The current number of output channels of the processor\n     */\n    void crossfade_output(const ChunkSampleBuffer& input_buffer, ChunkSampleBuffer& output_buffer,\n                          int input_channels, int output_channels);\n\n    /**\n     * @brief Calculate the ramp start and end value for the current chunk\n     * @return An std::pair with start and end gain levels\n     */\n    std::pair<float, float> get_ramp();\n\n    [[nodiscard]] int chunks_to_ramp(float sample_rate) const\n    {\n        return static_cast<int>(std::floor(std::max(1.0f, (sample_rate * _ramp_time.count() / AUDIO_CHUNK_SIZE))));\n    }\n\nprivate:\n    enum class BypassState\n    {\n        NOT_BYPASSED,\n        BYPASSED,\n        RAMPING_DOWN,\n        RAMPING_UP\n    };\n\n    BypassState _state{BypassState::NOT_BYPASSED};\n    std::chrono::duration<float, std::ratio<1,1>> _ramp_time{std::chrono::milliseconds(10)};\n    int _ramp_chunks{0};\n    int _ramp_count{0};\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_PROCESSOR_H\n"
  },
  {
    "path": "src/library/processor_state.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Container class implementation for the full state of a processor.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"processor_state.h\"\n\nnamespace sushi::internal {\n\nProcessorState::~ProcessorState() = default;\nbool ProcessorState::has_binary_data() const\n{\n    return !_binary_data.empty();\n}\n\nvoid ProcessorState::set_program(int program_id)\n{\n    _program = program_id;\n}\n\nvoid ProcessorState::set_bypass(bool enabled)\n{\n    _bypassed = enabled;\n}\n\nvoid ProcessorState::add_parameter_change(ObjectId parameter_id, float value)\n{\n    _parameter_changes.emplace_back(parameter_id, value);\n}\n\nvoid ProcessorState::add_property_change(ObjectId property_id, const std::string& value)\n{\n    _property_changes.emplace_back(property_id, value);\n}\n\nvoid ProcessorState::set_binary_data(const std::vector<std::byte>& data)\n{\n    _binary_data = data;\n}\n\nvoid ProcessorState::set_binary_data(std::vector<std::byte>&& data)\n{\n    _binary_data = data;\n}\n\nstd::optional<int> ProcessorState::program() const\n{\n    return _program;\n}\n\nstd::optional<bool> ProcessorState::bypassed() const\n{\n    return _bypassed;\n}\n\nconst std::vector<std::pair<ObjectId, float>>& ProcessorState::parameters() const\n{\n    return _parameter_changes;\n}\n\nconst std::vector<std::pair<ObjectId, std::string>>& ProcessorState::properties() const\n{\n    return _property_changes;\n}\n\nconst std::vector<std::byte>& ProcessorState::binary_data() const\n{\n    return _binary_data;\n}\n\nstd::vector<std::byte>& ProcessorState::binary_data()\n{\n    return _binary_data;\n}\n\n\nRtState::RtState() = default;\n\nRtState::RtState(const ProcessorState& state)\n{\n    _bypassed = state.bypassed();\n    _parameter_changes = state.parameters();\n}\n\nRtState::~RtState() = default;\n\nvoid RtState::set_bypass(bool enabled)\n{\n    _bypassed = enabled;\n}\n\nvoid RtState::add_parameter_change(ObjectId parameter_id, float value)\n{\n    _parameter_changes.emplace_back(parameter_id, value);\n}\n\nstd::optional<int> RtState::bypassed() const\n{\n    return _bypassed;\n}\n\nconst std::vector<std::pair<ObjectId, float>>& RtState::parameters() const\n{\n    return _parameter_changes;\n}\n\n} // end namespace sushi::internal\n"
  },
  {
    "path": "src/library/processor_state.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Container class for the full state of a processor.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_PROCESSOR_STATE_H\n#define SUSHI_PROCESSOR_STATE_H\n\n#include <optional>\n#include <vector>\n\n#include \"sushi/constants.h\"\n#include \"sushi/types.h\"\n\n#include \"library/id_generator.h\"\n#include \"engine/host_control.h\"\n\nnamespace sushi::internal {\n\nclass ProcessorState\n{\npublic:\n    virtual ~ProcessorState();\n\n\n    /**\n     * @brief  Check if the state contains opaque binary data from a plugin\n     *         If false, this means the state is described fully by the parameter and\n     *         property values. If true then the parameter and property values of this\n     *         object are empty and the values stored in the binary data.\n     * @return True if the instance contains binary data.\n     */\n    bool has_binary_data() const;\n\n    /**\n     * @brief Set the program id of the state.\n     * @param program_id The program id to store\n     */\n    void set_program(int program_id);\n\n    /**\n     * @brief Set the bypass state.\n     * @param program_id The bypass state id to store\n     */\n    void set_bypass(bool enabled);\n\n    /**\n     * @brief Store a parameter value in the state\n     * @param parameter_id The id of the parameter\n     * @param value The normalized float value\n     */\n    void add_parameter_change(ObjectId parameter_id, float value);\n\n    /**\n     * @brief Store a property value in the state\n     * @param propery_id The id of the property\n     * @param value The string value of the property\n     */\n    void add_property_change(ObjectId property_id, const std::string& value);\n\n    /**\n     * @brief Store binary data in the state\n     * @param data A vector of binary data to be copied into the state\n     */\n    void set_binary_data(const std::vector<std::byte>& data);\n\n    /**\n     * @brief Store binary data in the state\n     * @param data A vector of binary data to be moved into the state\n     */\n    void set_binary_data(std::vector<std::byte>&& data);\n\n    /**\n     * @brief Return the stored program id, if any.\n     * @return An std::optional<int> populated with the program id, if stored.\n     */\n    std::optional<int> program() const;\n\n    /**\n     * @brief Return the stored bypass state, if any.\n     * @return An std::optional<bool> populated with the bypass state, if stored.\n     */\n    std::optional<bool> bypassed() const;\n\n    /**\n     * @brief Return stored parameter values.\n     * @return A vector of id and value pairs\n     */\n    const std::vector<std::pair<ObjectId, float>>& parameters() const;\n\n    /**\n     * @brief Return stored property values.\n     * @return A vector of id and value pairs\n     */\n    const std::vector<std::pair<ObjectId, std::string>>& properties() const;\n\n    /**\n     * @brief Return stored binary data.\n     * @return A vector byte data for the plugin to interpret\n     */\n    const std::vector<std::byte>& binary_data() const;\n\n    std::vector<std::byte>& binary_data();\n\nprotected:\n    std::optional<int> _program;\n    std::optional<bool> _bypassed;\n    std::vector<std::pair<ObjectId, float>> _parameter_changes;\n    std::vector<std::pair<ObjectId, std::string>> _property_changes;\n    std::vector<std::byte> _binary_data;\n};\n\nclass RtState : public RtDeletable\n{\npublic:\n    RtState();\n    RtState(const ProcessorState& state);\n\n    ~RtState() override;\n\n    /**\n     * @brief Set the bypass state.\n     * @param program_id The bypass state id to store\n     */\n    void set_bypass(bool enabled);\n\n    /**\n     * @brief Store a parameter value in the state\n     * @param parameter_id The id of the parameter\n     * @param value The normalized float value\n     */\n    void add_parameter_change(ObjectId parameter_id, float value);\n\n    /**\n     * @brief Return the stored bypass state, if any.\n     * @return An std::optional<int> populated with the bypass state, if stored.\n     */\n    std::optional<int> bypassed() const;\n\n    /**\n     * @brief Return stored parameter values.\n     * @return A vector of id and value pairs\n     */\n    const std::vector<std::pair<ObjectId, float>>& parameters() const;\n\nprotected:\n    std::optional<int> _bypassed;\n    std::vector<std::pair<ObjectId, float>> _parameter_changes;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_PROCESSOR_STATE_H\n"
  },
  {
    "path": "src/library/rt_event.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Set of compact and performance oriented events for use in the realtime part.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_RT_EVENTS_H\n#define SUSHI_RT_EVENTS_H\n\n#include <string>\n#include <cassert>\n#include <optional>\n\n#include \"sushi/types.h\"\n#include \"sushi/sushi_time.h\"\n\n#include \"id_generator.h\"\n#include \"library/connection_types.h\"\n#include \"library/midi_decoder.h\"\n\nnamespace sushi::internal {\n\n/* Currently limiting the size of an event to 32 bytes and forcing it to align\n * to 32 byte boundaries. We could possibly extend this to 64 bytes if neccesary,\n * but likely not further */\n#ifndef SUSHI_EVENT_CACHE_ALIGNMENT\n#define SUSHI_EVENT_CACHE_ALIGNMENT 32\n#endif\n\nclass RtEvent;\ninline bool is_keyboard_event(const RtEvent& event);\ninline bool is_engine_control_event(const RtEvent& event);\ninline bool is_returnable_event(const RtEvent& event);\n\n\n/**\n * List of realtime message types\n */\nenum class RtEventType\n{\n    /* Processor commands */\n    NOTE_ON,\n    NOTE_OFF,\n    NOTE_AFTERTOUCH,\n    PITCH_BEND,\n    AFTERTOUCH,\n    MODULATION,\n    WRAPPED_MIDI_EVENT,\n    GATE_EVENT,\n    CV_EVENT,\n    INT_PARAMETER_CHANGE,\n    FLOAT_PARAMETER_CHANGE,\n    BOOL_PARAMETER_CHANGE,\n    DATA_PROPERTY_CHANGE,\n    STRING_PROPERTY_CHANGE,\n    SET_BYPASS,\n    SET_STATE,\n    DELETE,\n    NOTIFY,\n    /* Engine commands */\n    TEMPO,\n    TIME_SIGNATURE,\n    PLAYING_MODE,\n    SYNC_MODE,\n    /* Processor add/delete/reorder events */\n    INSERT_PROCESSOR,\n    REMOVE_PROCESSOR,\n    ADD_PROCESSOR_TO_TRACK,\n    REMOVE_PROCESSOR_FROM_TRACK,\n    ADD_TRACK,\n    REMOVE_TRACK,\n    ASYNC_WORK,\n    ASYNC_WORK_NOTIFICATION,\n    /* Routing events */\n    ADD_AUDIO_CONNECTION,\n    REMOVE_AUDIO_CONNECTION,\n    ADD_CV_CONNECTION,\n    REMOVE_CV_CONNECTION,\n    ADD_GATE_CONNECTION,\n    REMOVE_GATE_CONNECTION,\n    /* Delete object event */\n    BLOB_DELETE,\n    /* Synchronisation events */\n    SYNC,\n    TIMING_TICK,\n    /* Engine notification events */\n    CLIP_NOTIFICATION,\n};\n\nclass BaseRtEvent\n{\npublic:\n    /**\n     * @brief Type of event.\n     * @return\n     */\n    RtEventType type() const {return _type;}\n\n    /**\n     * @brief The processor id of the target for this message.\n     * @return\n     */\n    ObjectId processor_id() const {return _processor_id;}\n\n    /**\n     * @brief For real time events that need sample accurate timing, how many\n     *        samples into the current chunk the event should take effect.\n     * @return\n     */\n    int sample_offset() const {return _sample_offset;}\n\nprotected:\n    BaseRtEvent(RtEventType type, ObjectId target, int offset) : _type(type),\n                                                                 _processor_id(target),\n                                                                 _sample_offset(offset) {}\n    RtEventType _type;\n    ObjectId _processor_id;\n    int _sample_offset;\n};\n\n/**\n * @brief Event class for all keyboard events.\n */\nclass KeyboardRtEvent : public BaseRtEvent\n{\npublic:\n    KeyboardRtEvent(RtEventType type,\n                    ObjectId target,\n                    int offset,\n                    int channel,\n                    int note,\n                    float velocity) : BaseRtEvent(type, target, offset),\n                                      _channel(channel),\n                                      _note(note),\n                                      _velocity(velocity)\n    {\n        assert(type == RtEventType::NOTE_ON ||\n               type == RtEventType::NOTE_OFF ||\n               type == RtEventType::NOTE_AFTERTOUCH);\n    }\n    int channel() const {return _channel;}\n    int note() const {return _note;}\n    float velocity() const {return _velocity;}\n\nprotected:\n    int _channel;\n    int _note;\n    float _velocity;\n};\n\nclass KeyboardCommonRtEvent : public BaseRtEvent\n{\npublic:\n    KeyboardCommonRtEvent(RtEventType type,\n                          ObjectId target,\n                          int offset,\n                          int channel,\n                          float value) : BaseRtEvent(type, target, offset),\n                                         _channel(channel),\n                                         _value(value)\n    {\n        assert(type == RtEventType::AFTERTOUCH ||\n               type == RtEventType::PITCH_BEND ||\n               type == RtEventType::MODULATION);\n    }\n    int channel() const {return _channel;}\n    float value() const {return _value;}\n\nprotected:\n    int _channel;\n    float _value;\n};\n\n/**\n * @brief This provides a way of \"tunneling\" raw midi for plugins that\n * handle midi natively. Could come in handy, or me might duplicate the entire\n * midi functionality in our own events.\n */\nclass WrappedMidiRtEvent : public BaseRtEvent\n{\npublic:\n    WrappedMidiRtEvent(int offset,\n                       ObjectId target,\n                       MidiDataByte data) : BaseRtEvent(RtEventType::WRAPPED_MIDI_EVENT, target, offset),\n                                            _midi_data{data} {}\n\n    MidiDataByte midi_data() const {return _midi_data;}\n\nprotected:\n    MidiDataByte _midi_data;\n};\n\nclass GateRtEvent : public BaseRtEvent\n{\npublic:\n    GateRtEvent(ObjectId target,\n                int offset,\n                int gate_id,\n                bool value) : BaseRtEvent(RtEventType::GATE_EVENT, target, offset),\n                                          _gate_id(gate_id),\n                                          _value(value) {}\n\n    int gate_no() const {return _gate_id;}\n    bool value() const {return _value;}\n\nprotected:\n    int _gate_id;\n    bool _value;\n};\n\nclass CvRtEvent : public BaseRtEvent\n{\npublic:\n    CvRtEvent(ObjectId target,\n                int offset,\n                int cv_id,\n                float value) : BaseRtEvent(RtEventType::CV_EVENT, target, offset),\n                               _cv_id(cv_id),\n                               _value(value) {}\n\n    int cv_id() const {return _cv_id;}\n    float value() const {return _value;}\n\nprotected:\n    int _cv_id;\n    float _value;\n};\n\n\n/**\n * @brief Baseclass for simple parameter changes\n */\nclass ParameterChangeRtEvent : public BaseRtEvent\n{\npublic:\n    ParameterChangeRtEvent(RtEventType type,\n                           ObjectId target,\n                           int offset,\n                           ObjectId param_id,\n                           float value) : BaseRtEvent(type, target, offset),\n                                          _param_id(param_id),\n                                          _value(value)\n    {\n        assert(type == RtEventType::FLOAT_PARAMETER_CHANGE ||\n               type == RtEventType::INT_PARAMETER_CHANGE ||\n               type == RtEventType::BOOL_PARAMETER_CHANGE);\n    }\n    ObjectId param_id() const {return _param_id;}\n\n    float value() const {return _value;}\n\nprotected:\n    ObjectId _param_id;\n    float _value;\n};\n\n\n/**\n * @brief Baseclass for events that need to carry a larger payload of data.\n */\nclass DataPayloadRtEvent : public BaseRtEvent\n{\npublic:\n    DataPayloadRtEvent(RtEventType type,\n                       ObjectId processor,\n                       int offset,\n                       BlobData data) : BaseRtEvent(type, processor, offset),\n                                        _data_size(data.size),\n                                        _data(data.data) {}\n\n    BlobData value() const\n    {\n        return BlobData{_data_size, _data};\n    }\n\nprotected:\n    /* The members of BlobData are pulled apart and stored separately\n     * here because this enables us to pack the members efficiently using only\n     * self-aligning without having to resort to non-portable #pragmas or\n     * __attributes__  to keep the size of the event small */\n    int _data_size;\n    uint8_t* _data;\n};\n\n/**\n * @brief Class for string parameter changes\n */\nclass PropertyChangeRtEvent : public BaseRtEvent\n{\npublic:\n    PropertyChangeRtEvent(ObjectId processor,\n                          int offset,\n                          ObjectId param_id,\n                          RtDeletableWrapper<std::string>* value) : BaseRtEvent(RtEventType::STRING_PROPERTY_CHANGE,\n                                                                                processor,\n                                                                                offset),\n                                                                   _data(value),\n                                                                   _param_id(param_id) {}\n\n    ObjectId param_id() const {return _param_id;}\n\n    std::string* value() const {return &_data->data();}\n    RtDeletable* deletable_value() const {return _data;}\n\nprotected:\n    RtDeletableWrapper<std::string>* _data;\n    ObjectId _param_id;\n};\n\n\n/**\n * @brief Class for binarydata parameter changes\n */\nclass DataPropertyChangeRtEvent : public DataPayloadRtEvent\n{\npublic:\n    DataPropertyChangeRtEvent(ObjectId processor,\n                               int offset,\n                               ObjectId param_id,\n                               BlobData value) : DataPayloadRtEvent(RtEventType::DATA_PROPERTY_CHANGE,\n                                                                    processor,\n                                                                    offset,\n                                                                    value),\n                                                 _param_id(param_id) {}\n\n    ObjectId param_id() const {return _param_id;}\n\nprotected:\n    ObjectId _param_id;\n};\n\n/**\n * @brief Class for sending commands to processors.\n */\nclass ProcessorCommandRtEvent : public BaseRtEvent\n{\npublic:\n    ProcessorCommandRtEvent(RtEventType type,\n                            ObjectId processor,\n                            int value) : BaseRtEvent(type, processor, 0),\n                                         _value(value)\n    {\n        assert(type == RtEventType::SET_BYPASS ||\n               type == RtEventType::ASYNC_WORK_NOTIFICATION );\n    }\n    int value() const {return _value;}\nprivate:\n    int _value;\n};\n\nclass RtState;\n/**\n * @brief Class for binarydata parameter changes\n */\nclass ProcessorStateRtEvent : public BaseRtEvent\n{\npublic:\n    ProcessorStateRtEvent(ObjectId processor,\n                          RtState* state) : BaseRtEvent(RtEventType::SET_STATE, processor, 0),\n                                            _state(state) {}\n\n    RtState* state() const {return _state;}\n\nprotected:\n    RtState* _state;\n};\n\n/**\n * @brief Class for sending notifications from the rt thread of a processor\n */\nclass ProcessorNotifyRtEvent : public BaseRtEvent\n{\npublic:\n    enum class Action\n    {\n        PARAMETER_UPDATE\n    };\n    ProcessorNotifyRtEvent(ObjectId processor,\n                           Action action) : BaseRtEvent(RtEventType::NOTIFY, processor, 0),\n                                            _action(action) {}\n\n    Action action() const {return _action;}\n\nprotected:\n    Action _action;\n};\n\n\n/**\n * @brief Baseclass for events that can be returned with a status code.\n */\nclass ReturnableRtEvent : public BaseRtEvent\n{\npublic:\n    enum class EventStatus : uint8_t\n    {\n        UNHANDLED,\n        HANDLED_OK,\n        HANDLED_ERROR\n    };\n    ReturnableRtEvent(RtEventType type, ObjectId processor) : BaseRtEvent(type, processor, 0),\n                                                              _status{EventStatus::UNHANDLED},\n                                                              _event_id{EventIdGenerator::new_id()} {}\n\n    EventStatus status() const {return _status;}\n    uint16_t event_id() const {return _event_id;}\n    void set_handled(bool ok) {_status = ok? EventStatus::HANDLED_OK : EventStatus::HANDLED_ERROR;}\n\nprotected:\n    EventStatus _status;\n    uint16_t _event_id;\n};\n\nclass Processor;\n\nclass ProcessorOperationRtEvent : public ReturnableRtEvent\n{\npublic:\n    ProcessorOperationRtEvent(RtEventType type,\n                              Processor* instance) : ReturnableRtEvent(type, 0),\n                                                     _instance{instance} {}\n    Processor* instance() const {return _instance;}\nprivate:\n    Processor* _instance;\n};\n\nclass TrackRtEvent : public ReturnableRtEvent\n{\npublic:\n    TrackRtEvent(RtEventType type,\n                 ObjectId track,\n                 std::optional<int> thread) : ReturnableRtEvent(type, 0),\n                                              _track{track},\n                                              _thread{thread} {}\n\n    ObjectId track() const {return _track;}\n    std::optional<int> thread() const {return _thread;}\nprivate:\n    ObjectId _track;\n    std::optional<int> _thread;\n};\n\nclass ProcessorReorderRtEvent : public ReturnableRtEvent\n{\npublic:\n    ProcessorReorderRtEvent(RtEventType type,\n                            ObjectId processor,\n                            ObjectId track,\n                            std::optional<ObjectId> before_processor) : ReturnableRtEvent(type, 0),\n                                                                        _processor{processor},\n                                                                        _track{track},\n                                                                        _before_processor{before_processor} {}\n\n    ObjectId processor() const {return _processor;}\n    ObjectId track() const {return _track;}\n    const std::optional<ObjectId>& before_processor() const {return _before_processor;}\n\nprivate:\n    ObjectId _processor;\n    ObjectId _track;\n    std::optional<ObjectId> _before_processor;\n};\n\ntypedef int (*AsyncWorkCallback)(void* data, EventId id);\n\nclass AsyncWorkRtEvent: public ReturnableRtEvent\n{\npublic:\n    AsyncWorkRtEvent(AsyncWorkCallback callback, ObjectId processor, void* data) : ReturnableRtEvent(RtEventType::ASYNC_WORK,\n                                                                                                     processor),\n                                                                                   _callback{callback},\n                                                                                   _data{data} {}\n    AsyncWorkCallback callback() const {return _callback;}\n    void*             callback_data() const {return _data;}\nprivate:\n    AsyncWorkCallback _callback;\n    void*             _data;\n};\n\nclass AsyncWorkRtCompletionEvent : public ProcessorCommandRtEvent\n{\npublic:\n    AsyncWorkRtCompletionEvent(ObjectId processor,\n                               uint16_t event_id,\n                               int return_status) : ProcessorCommandRtEvent(RtEventType::ASYNC_WORK_NOTIFICATION,\n                                                                            processor,\n                                                                            return_status),\n                                                    _event_id{event_id} {}\n\n    uint16_t    sending_event_id() const {return _event_id;}\n    int         return_status() const {return value();}\n\nprivate:\n    uint16_t  _event_id;\n};\n\n/* Base class for passing audio, cv and gate connections */\n/* slightly hackish to repurpose the processor_id field for storing a\n * bool, but it allows us to keep the size down to 32 bytes.\n * Otherwise GateConnection wouldn't fit in the event */\ntemplate <typename ConnectionType>\nclass ConnectionRtEvent : public ReturnableRtEvent\n{\npublic:\n    ConnectionRtEvent(ConnectionType connection,\n                      RtEventType type,\n                      bool is_input_connection) : ReturnableRtEvent(type,\n                                                                    is_input_connection ? 1 : 0),\n                                                  _connection(connection) {}\n\n    const ConnectionType& connection() const {return _connection;}\n    bool  input_connection() const {return _processor_id == 1;}\n    bool  output_connection() const {return _processor_id == 0;}\n\nprivate:\n    ConnectionType _connection;\n};\n\n/* RtEvents for passing and deleting audio, cv and gate mappings */\nusing AudioConnectionRtEvent = ConnectionRtEvent<AudioConnection>;\nusing CvConnectionRtEvent = ConnectionRtEvent<CvConnection>;\nusing GateConnectionRtEvent = ConnectionRtEvent<GateConnection>;\n\n\n/* RtEvent for passing timestamps synced to sample offsets */\nclass SynchronisationRtEvent : public BaseRtEvent\n{\npublic:\n    SynchronisationRtEvent(Time timestamp) : BaseRtEvent(RtEventType::SYNC, 0, 0),\n                                             _timestamp(timestamp) {}\n\n    Time timestamp() const {return _timestamp;}\n\nprotected:\n    Time _timestamp;\n};\n\n/* RtEvent for passing tempo information */\nclass TempoRtEvent : public BaseRtEvent\n{\npublic:\n    TempoRtEvent(int offset, float tempo) : BaseRtEvent(RtEventType::TEMPO, 0, offset),\n                                            _tempo(tempo) {}\n\n    float tempo() const {return _tempo;}\n\nprotected:\n    float _tempo;\n};\n\n/* RtEvent for passing time signature information */\nclass TimeSignatureRtEvent : public BaseRtEvent\n{\npublic:\n    TimeSignatureRtEvent(int offset, TimeSignature signature) : BaseRtEvent(RtEventType::TIME_SIGNATURE, 0, offset),\n                                                                _signature(signature) {}\n\n    TimeSignature time_signature() const {return _signature;}\n\nprotected:\n    TimeSignature _signature;\n};\n\nenum class PlayingMode\n{\n    STOPPED,\n    PLAYING,\n    RECORDING\n};\n\n/* RtEvent for passing transport commands */\nclass PlayingModeRtEvent : public BaseRtEvent\n{\npublic:\n    PlayingModeRtEvent(int offset, PlayingMode mode) : BaseRtEvent(RtEventType::PLAYING_MODE, 0, offset),\n                                                       _mode(mode) {}\n\n    PlayingMode mode() const {return _mode;}\n\nprotected:\n    PlayingMode _mode;\n};\n\nenum class SyncMode\n{\n    INTERNAL,\n    MIDI,\n    GATE_INPUT,\n    ABLETON_LINK\n};\n\n/* RtEvent for setting the mode of external tempo sync */\nclass SyncModeRtEvent : public BaseRtEvent\n{\npublic:\n    SyncModeRtEvent(int offset, SyncMode mode) : BaseRtEvent(RtEventType::SYNC_MODE, 0, offset),\n                                                 _mode(mode) {}\n\n    SyncMode mode() const {return _mode;}\n\nprotected:\n    SyncMode _mode;\n};\n\n/* RtEvent for sending transport timing ticks for tempo sync */\nclass TimingTickRtEvent : public BaseRtEvent\n{\npublic:\n    TimingTickRtEvent(int offset, int tick_count) : BaseRtEvent(RtEventType::TIMING_TICK, 0, offset),\n                                                    _tick_count(tick_count) {}\n\n    int tick_count() const {return _tick_count;}\nprotected:\n    int _tick_count;\n};\n\n\n/* RtEvent for notifing the engine of audio clipping in the realtime */\nclass ClipNotificationRtEvent : public BaseRtEvent\n{\npublic:\n    enum class ClipChannelType\n    {\n        INPUT,\n        OUTPUT,\n    };\n    ClipNotificationRtEvent(int offset, int channel, ClipChannelType channel_type) : BaseRtEvent(RtEventType::CLIP_NOTIFICATION,\n                                                                                                 0,\n                                                                                                 offset),\n                                                                                     _channel(channel),\n                                                                                     _channel_type(channel_type) {}\n\n    int channel() const {return _channel;}\n    ClipChannelType channel_type() const {return _channel_type;}\n\nprivate:\n    int _channel;\n    ClipChannelType _channel_type;\n};\n\n/**\n * @brief Class for passing deletable data out from the rt domain\n */\nclass DeleteDataRtEvent : public BaseRtEvent\n{\npublic:\n    DeleteDataRtEvent(RtDeletable* data) : BaseRtEvent(RtEventType::DELETE, 0, 0),\n                                           _data(data)\n    {}\n\n    RtDeletable* data() const {return _data;}\n\nprivate:\n    RtDeletable* _data;\n};\n\n/**\n * @brief Container class for rt events. Functionally this takes the role of a\n *        baseclass for events, from which you can access the derived event\n *        classes via function calls that essentially casts the event to the\n *        given rt event type.\n */\nclass alignas(SUSHI_EVENT_CACHE_ALIGNMENT) RtEvent\n{\npublic:\n    RtEvent() {};\n\n    RtEventType type() const {return _base_event.type();}\n\n    ObjectId processor_id() const {return _base_event.processor_id();}\n\n    int sample_offset() const {return _base_event.sample_offset();}\n\n    /* Access functions protected by asserts */\n    const KeyboardRtEvent* keyboard_event() const\n    {\n        assert(_keyboard_event.type() == RtEventType::NOTE_ON ||\n               _keyboard_event.type() == RtEventType::NOTE_OFF ||\n               _keyboard_event.type() == RtEventType::NOTE_AFTERTOUCH);\n        return &_keyboard_event;\n    }\n\n    const KeyboardCommonRtEvent* keyboard_common_event() const\n    {\n        assert(_keyboard_event.type() == RtEventType::AFTERTOUCH ||\n               _keyboard_event.type() == RtEventType::PITCH_BEND ||\n               _keyboard_event.type() == RtEventType::MODULATION);\n        return &_keyboard_common_event;\n    }\n\n    const WrappedMidiRtEvent* wrapped_midi_event() const\n    {\n        assert(_wrapped_midi_event.type() == RtEventType::WRAPPED_MIDI_EVENT);\n        return &_wrapped_midi_event;\n    }\n\n    const GateRtEvent* gate_event() const\n    {\n        assert(_gate_event.type() == RtEventType::GATE_EVENT);\n        return &_gate_event;\n    }\n\n    const CvRtEvent* cv_event() const\n    {\n        assert(_cv_event.type() == RtEventType::CV_EVENT);\n        return &_cv_event;\n    }\n\n    const ParameterChangeRtEvent* parameter_change_event() const\n    {\n        assert(_keyboard_event.type() == RtEventType::FLOAT_PARAMETER_CHANGE);\n        return &_parameter_change_event;\n    }\n    const PropertyChangeRtEvent* property_change_event() const\n    {\n        assert(_property_change_event.type() == RtEventType::STRING_PROPERTY_CHANGE);\n        return &_property_change_event;\n    }\n    const DataPropertyChangeRtEvent* data_parameter_change_event() const\n    {\n        assert(_data_property_change_event.type() == RtEventType::DATA_PROPERTY_CHANGE);\n        return &_data_property_change_event;\n    }\n\n    const ProcessorCommandRtEvent* processor_command_event() const\n    {\n        assert(_processor_command_event.type() == RtEventType::SET_BYPASS);\n        return &_processor_command_event;\n    }\n\n    const ProcessorStateRtEvent* processor_state_event() const\n    {\n        assert(_processor_state_event.type() == RtEventType::SET_STATE);\n        return &_processor_state_event;\n    }\n\n    const ProcessorNotifyRtEvent* processor_notify_event() const\n    {\n        assert(_processor_notify_event.type() == RtEventType::NOTIFY);\n        return &_processor_notify_event;\n    }\n\n    // ReturnableEvent and every event type that inherits from it\n    // needs a non-const accessor function as well\n    const ReturnableRtEvent* returnable_event() const\n    {\n        assert(is_returnable_event(*this));\n        return &_returnable_event;\n    }\n\n    ReturnableRtEvent* returnable_event()\n    {\n        assert(is_returnable_event(*this));\n        return &_returnable_event;\n    }\n\n    const TrackRtEvent* track_event() const\n    {\n        assert(_track_event.type() == RtEventType::ADD_TRACK ||\n               _track_event.type() == RtEventType::REMOVE_TRACK);\n        return &_track_event;\n    }\n\n    TrackRtEvent* track_event()\n    {\n        assert(_track_event.type() == RtEventType::ADD_TRACK ||\n               _track_event.type() == RtEventType::REMOVE_TRACK);\n        return &_track_event;\n    }\n\n    const ProcessorOperationRtEvent* processor_operation_event() const\n    {\n        assert(_processor_operation_event.type() == RtEventType::INSERT_PROCESSOR);\n        return &_processor_operation_event;\n    }\n\n    ProcessorOperationRtEvent* processor_operation_event()\n    {\n        assert(_processor_operation_event.type() == RtEventType::INSERT_PROCESSOR);\n        return &_processor_operation_event;\n    }\n\n    const ProcessorReorderRtEvent* processor_reorder_event() const\n    {\n        assert(_processor_reorder_event.type() == RtEventType::REMOVE_PROCESSOR ||\n               _processor_reorder_event.type() == RtEventType::ADD_PROCESSOR_TO_TRACK ||\n               _processor_reorder_event.type() == RtEventType::REMOVE_PROCESSOR_FROM_TRACK);\n        ;\n        return &_processor_reorder_event;\n    }\n\n    ProcessorReorderRtEvent* processor_reorder_event()\n    {\n        assert(_processor_reorder_event.type() == RtEventType::REMOVE_PROCESSOR ||\n               _processor_reorder_event.type() == RtEventType::ADD_PROCESSOR_TO_TRACK ||\n               _processor_reorder_event.type() == RtEventType::REMOVE_PROCESSOR_FROM_TRACK)\n        ;\n        return &_processor_reorder_event;\n    }\n\n    const AsyncWorkRtEvent* async_work_event() const\n    {\n        assert(_async_work_event.type() == RtEventType::ASYNC_WORK);\n        return &_async_work_event;\n    }\n\n    AsyncWorkRtEvent* async_work_event()\n    {\n        assert(_async_work_event.type() == RtEventType::ASYNC_WORK);\n        return &_async_work_event;\n    }\n\n    const AsyncWorkRtCompletionEvent* async_work_completion_event() const\n    {\n        assert(_async_work_completion_event.type() == RtEventType::ASYNC_WORK_NOTIFICATION);\n        return &_async_work_completion_event;\n    }\n\n    const AudioConnectionRtEvent* audio_connection_event() const\n    {\n        assert(_audio_connection_event.type() == RtEventType::ADD_AUDIO_CONNECTION ||\n               _audio_connection_event.type() == RtEventType::REMOVE_AUDIO_CONNECTION);\n        return &_audio_connection_event;\n    }\n\n    AudioConnectionRtEvent* audio_connection_event()\n    {\n        assert(_audio_connection_event.type() == RtEventType::ADD_AUDIO_CONNECTION ||\n               _audio_connection_event.type() == RtEventType::REMOVE_AUDIO_CONNECTION);\n        return &_audio_connection_event;\n    }\n\n    const CvConnectionRtEvent* cv_connection_event() const\n    {\n        assert(_cv_connection_event.type() == RtEventType::ADD_CV_CONNECTION ||\n               _cv_connection_event.type() == RtEventType::REMOVE_CV_CONNECTION);\n        return &_cv_connection_event;\n    }\n\n    const GateConnectionRtEvent* gate_connection_event() const\n    {\n        assert(_gate_connection_event.type() == RtEventType::ADD_GATE_CONNECTION ||\n               _gate_connection_event.type() == RtEventType::REMOVE_GATE_CONNECTION);\n        return &_gate_connection_event;\n    }\n\n    const DataPayloadRtEvent* data_payload_event() const\n    {\n        assert(_data_payload_event.type() == RtEventType::BLOB_DELETE);\n        return &_data_payload_event;\n    }\n\n    const SynchronisationRtEvent* syncronisation_event() const\n    {\n        assert(_synchronisation_event.type() == RtEventType::SYNC);\n        return &_synchronisation_event;\n    }\n\n    const TempoRtEvent* tempo_event() const\n    {\n        assert(_tempo_event.type() == RtEventType::TEMPO);\n        return &_tempo_event;\n    }\n\n    const TimeSignatureRtEvent* time_signature_event() const\n    {\n        assert(_time_signature_event.type() == RtEventType::TIME_SIGNATURE);\n        return &_time_signature_event;\n    }\n\n    const PlayingModeRtEvent* playing_mode_event() const\n    {\n        assert(_playing_mode_event.type() == RtEventType::PLAYING_MODE);\n        return &_playing_mode_event;\n    }\n\n    const SyncModeRtEvent* sync_mode_event() const\n    {\n        assert(_sync_mode_event.type() == RtEventType::SYNC_MODE);\n        return &_sync_mode_event;\n    }\n\n    const TimingTickRtEvent* timing_tick_event() const\n    {\n        assert(_timing_tick_event.type() == RtEventType::TIMING_TICK);\n        return &_timing_tick_event;\n    }\n\n    const ClipNotificationRtEvent* clip_notification_event() const\n    {\n        assert(_clip_notification_event.type() == RtEventType::CLIP_NOTIFICATION);\n        return &_clip_notification_event;\n    }\n\n    const DeleteDataRtEvent* delete_data_event() const\n    {\n        assert(_delete_data_event.type() == RtEventType::DELETE);\n        return &_delete_data_event;\n    }\n\n    /* Factory functions for constructing events */\n    static RtEvent make_note_on_event(ObjectId target, int offset, int channel, int note, float velocity)\n    {\n        return make_keyboard_event(RtEventType::NOTE_ON, target, offset, channel, note, velocity);\n    }\n\n    static RtEvent make_note_on_event(ObjectId target, int offset, midi::NoteOnMessage msg)\n    {\n        return make_keyboard_event(RtEventType::NOTE_ON, target, offset, msg.channel, msg.note, msg.velocity / midi::MAX_VALUE_F);\n    }\n\n    static RtEvent make_note_off_event(ObjectId target, int offset, int channel, int note, float velocity)\n    {\n        return make_keyboard_event(RtEventType::NOTE_OFF, target, offset, channel, note, velocity);\n    }\n\n    static RtEvent make_note_off_event(ObjectId target, int offset, midi::NoteOffMessage msg)\n    {\n        return make_keyboard_event(RtEventType::NOTE_OFF, target, offset, msg.channel, msg.note, msg.velocity / midi::MAX_VALUE_F);\n    }\n\n    static RtEvent make_note_aftertouch_event(ObjectId target, int offset, int channel, int note, float velocity)\n    {\n        return make_keyboard_event(RtEventType::NOTE_AFTERTOUCH, target, offset, channel, note, velocity);\n    }\n\n    static RtEvent make_note_aftertouch_event(ObjectId target, int offset, midi::PolyKeyPressureMessage msg)\n    {\n        return make_keyboard_event(RtEventType::NOTE_AFTERTOUCH, target, offset, msg.channel, msg.note, msg.pressure / midi::MAX_VALUE_F);\n    }\n\n    static RtEvent make_keyboard_event(RtEventType type, ObjectId target, int offset, int channel, int note, float velocity)\n    {\n        KeyboardRtEvent typed_event(type, target, offset, channel, note, velocity);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_aftertouch_event(ObjectId target, int offset, int channel, float value)\n    {\n        return make_keyboard_common_event(RtEventType::AFTERTOUCH, target, offset, channel, value);\n    }\n\n    static RtEvent make_aftertouch_event(ObjectId target, int offset, midi::ChannelPressureMessage msg)\n    {\n        return make_keyboard_common_event(RtEventType::AFTERTOUCH, target, offset, msg.channel, msg.pressure / midi::MAX_VALUE_F);\n    }\n\n    static RtEvent make_pitch_bend_event(ObjectId target, int offset, int channel, float value)\n    {\n        return make_keyboard_common_event(RtEventType::PITCH_BEND, target, offset, channel, value);\n    }\n\n    static RtEvent make_pitch_bend_event(ObjectId target, int offset, midi::PitchBendMessage msg)\n    {\n        return make_keyboard_common_event(RtEventType::PITCH_BEND, target, offset, msg.channel, (msg.value / static_cast<float>(midi::PITCH_BEND_MIDDLE)) - 1.0f);\n    }\n\n    static RtEvent make_kb_modulation_event(ObjectId target, int offset, int channel, float value)\n    {\n        return make_keyboard_common_event(RtEventType::MODULATION, target, offset, channel, value);\n    }\n\n    static RtEvent make_kb_modulation_event(ObjectId target, int offset, midi::ControlChangeMessage msg)\n    {\n        return make_keyboard_common_event(RtEventType::MODULATION, target, offset, msg.channel, msg.value / midi::MAX_VALUE_F);\n    }\n\n    static RtEvent make_keyboard_common_event(RtEventType type, ObjectId target, int offset, int channel, float value)\n    {\n        KeyboardCommonRtEvent typed_event(type, target, offset, channel, value);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_gate_event(ObjectId target, int offset, int gate_id, bool value)\n    {\n        GateRtEvent typed_event(target, offset, gate_id, value);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_cv_event(ObjectId target, int offset, int cv_id, float value)\n    {\n        CvRtEvent typed_event(target, offset, cv_id, value);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_parameter_change_event(ObjectId target, int offset, ObjectId param_id, float value)\n    {\n        ParameterChangeRtEvent typed_event(RtEventType::FLOAT_PARAMETER_CHANGE, target, offset, param_id, value);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_wrapped_midi_event(ObjectId target, int offset, MidiDataByte data)\n    {\n        WrappedMidiRtEvent typed_event(offset, target, data);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_string_property_change_event(ObjectId target, int offset, ObjectId param_id, RtDeletableWrapper<std::string>* value)\n    {\n        PropertyChangeRtEvent typed_event(target, offset, param_id, value);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_data_property_change_event(ObjectId target, int offset, ObjectId param_id, BlobData data)\n    {\n        DataPropertyChangeRtEvent typed_event(target, offset, param_id, data);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_bypass_processor_event(ObjectId target, bool value)\n    {\n        ProcessorCommandRtEvent typed_event(RtEventType::SET_BYPASS, target, value);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_set_rt_state_event(ObjectId target, RtState* state)\n    {\n        ProcessorStateRtEvent typed_event(target, state);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_processor_notify_event(ObjectId target, ProcessorNotifyRtEvent::Action action)\n    {\n        ProcessorNotifyRtEvent typed_event(target, action);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_insert_processor_event(Processor* instance)\n    {\n        ProcessorOperationRtEvent typed_event(RtEventType::INSERT_PROCESSOR, instance);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_remove_processor_event(ObjectId processor)\n    {\n        ProcessorReorderRtEvent typed_event(RtEventType::REMOVE_PROCESSOR, processor, 0, std::nullopt);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_add_processor_to_track_event(ObjectId processor,\n                                                     ObjectId track,\n                                                     std::optional<ObjectId> before_processor = std::nullopt)\n    {\n        ProcessorReorderRtEvent typed_event(RtEventType::ADD_PROCESSOR_TO_TRACK, processor, track, before_processor);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_remove_processor_from_track_event(ObjectId processor, ObjectId track)\n    {\n        ProcessorReorderRtEvent typed_event(RtEventType::REMOVE_PROCESSOR_FROM_TRACK, processor, track, std::nullopt);\n        return RtEvent(typed_event);\n    }\n\n    static RtEvent make_add_track_event(ObjectId track, std::optional<int> thread)\n    {\n        return TrackRtEvent(RtEventType::ADD_TRACK, track, thread);\n    }\n\n    static RtEvent make_remove_track_event(ObjectId track)\n    {\n        return TrackRtEvent(RtEventType::REMOVE_TRACK, track, std::nullopt);\n    }\n\n    static RtEvent make_async_work_event(AsyncWorkCallback callback, ObjectId processor, void* data)\n    {\n        AsyncWorkRtEvent typed_event(callback, processor, data);\n        return typed_event;\n    }\n\n    static RtEvent make_async_work_completion_event(ObjectId processor, uint16_t event_id, int return_status)\n    {\n        AsyncWorkRtCompletionEvent typed_event(processor, event_id, return_status);\n        return typed_event;\n    }\n\n    static RtEvent make_add_audio_input_connection_event(const AudioConnection& connection)\n    {\n        AudioConnectionRtEvent typed_event(connection, RtEventType::ADD_AUDIO_CONNECTION, true);\n        return typed_event;\n    }\n\n    static RtEvent make_add_audio_output_connection_event(const AudioConnection& connection)\n    {\n        AudioConnectionRtEvent typed_event(connection, RtEventType::ADD_AUDIO_CONNECTION, false);\n        return typed_event;\n    }\n\n    static RtEvent make_remove_audio_input_connection_event(const AudioConnection& connection)\n    {\n        AudioConnectionRtEvent typed_event(connection, RtEventType::REMOVE_AUDIO_CONNECTION, true);\n        return typed_event;\n    }\n\n    static RtEvent make_remove_audio_output_connection_event(const AudioConnection& connection)\n    {\n        AudioConnectionRtEvent typed_event(connection, RtEventType::REMOVE_AUDIO_CONNECTION, false);\n        return typed_event;\n    }\n\n    static RtEvent make_add_cv_input_connection_event(const CvConnection& connection)\n    {\n        CvConnectionRtEvent typed_event(connection, RtEventType::ADD_CV_CONNECTION, true);\n        return typed_event;\n    }\n\n    static RtEvent make_add_cv_output_connection_event(const CvConnection& connection)\n    {\n        CvConnectionRtEvent typed_event(connection, RtEventType::ADD_CV_CONNECTION, false);\n        return typed_event;\n    }\n\n    static RtEvent make_remove_cv_input_connection_event(const CvConnection& connection)\n    {\n        CvConnectionRtEvent typed_event(connection, RtEventType::REMOVE_CV_CONNECTION, true);\n        return typed_event;\n    }\n\n    static RtEvent make_remove_cv_output_connection_event(const CvConnection& connection)\n    {\n        CvConnectionRtEvent typed_event(connection, RtEventType::REMOVE_CV_CONNECTION, false);\n        return typed_event;\n    }\n\n    static RtEvent make_add_gate_input_connection_event(const GateConnection& connection)\n    {\n        GateConnectionRtEvent typed_event(connection, RtEventType::ADD_GATE_CONNECTION, true);\n        return typed_event;\n    }\n\n    static RtEvent make_add_gate_output_connection_event(const GateConnection& connection)\n    {\n        GateConnectionRtEvent typed_event(connection, RtEventType::ADD_GATE_CONNECTION, false);\n        return typed_event;\n    }\n\n    static RtEvent make_remove_gate_input_connection_event(const GateConnection& connection)\n    {\n        GateConnectionRtEvent typed_event(connection, RtEventType::REMOVE_GATE_CONNECTION, true);\n        return typed_event;\n    }\n\n    static RtEvent make_remove_gate_output_connection_event(const GateConnection& connection)\n    {\n        GateConnectionRtEvent typed_event(connection, RtEventType::REMOVE_GATE_CONNECTION, false);\n        return typed_event;\n    }\n\n    static RtEvent make_delete_blob_event(BlobData data)\n    {\n        DataPayloadRtEvent typed_event(RtEventType::BLOB_DELETE, 0, 0, data);\n        return typed_event;\n    }\n\n    static RtEvent make_synchronisation_event(Time timestamp)\n    {\n        SynchronisationRtEvent typed_event(timestamp);\n        return typed_event;\n    }\n\n    static RtEvent make_tempo_event(int offset, float tempo)\n    {\n        TempoRtEvent typed_event(offset, tempo);\n        return typed_event;\n    }\n\n    static RtEvent make_time_signature_event(int offset, TimeSignature signature)\n    {\n        TimeSignatureRtEvent typed_event(offset, signature);\n        return typed_event;\n    }\n\n    static RtEvent make_playing_mode_event(int offset, PlayingMode mode)\n    {\n        PlayingModeRtEvent typed_event(offset, mode);\n        return typed_event;\n    }\n\n    static RtEvent make_sync_mode_event(int offset, SyncMode mode)\n    {\n        SyncModeRtEvent typed_event(offset, mode);\n        return typed_event;\n    }\n\n    static RtEvent make_timing_tick_event(int offset, int tick_count)\n    {\n        TimingTickRtEvent typed_event(offset, tick_count);\n        return typed_event;\n    }\n\n    static RtEvent make_clip_notification_event(int offset, int channel, ClipNotificationRtEvent::ClipChannelType type)\n    {\n        ClipNotificationRtEvent typed_event(offset, channel, type);\n        return typed_event;\n    }\n\n    static RtEvent make_delete_data_event(RtDeletable* data)\n    {\n        DeleteDataRtEvent typed_event(data);\n        return typed_event;\n    }\n\nprivate:\n    /* Private constructors that are invoked automatically when using the make_xxx_event functions */\n    RtEvent(const KeyboardRtEvent& e)                   : _keyboard_event(e) {}\n    RtEvent(const KeyboardCommonRtEvent& e)             : _keyboard_common_event(e) {}\n    RtEvent(const WrappedMidiRtEvent& e)                : _wrapped_midi_event(e) {}\n    RtEvent(const GateRtEvent& e)                       : _gate_event(e) {}\n    RtEvent(const CvRtEvent& e)                         : _cv_event(e) {}\n    RtEvent(const ParameterChangeRtEvent& e)            : _parameter_change_event(e) {}\n    RtEvent(const PropertyChangeRtEvent& e)             : _property_change_event(e) {}\n    RtEvent(const DataPropertyChangeRtEvent& e)         : _data_property_change_event(e) {}\n    RtEvent(const ProcessorCommandRtEvent& e)           : _processor_command_event(e) {}\n    RtEvent(const ProcessorStateRtEvent& e)             : _processor_state_event(e) {}\n    RtEvent(const ProcessorNotifyRtEvent& e)            : _processor_notify_event(e) {}\n    RtEvent(const ReturnableRtEvent& e)                 : _returnable_event(e) {}\n    RtEvent(const TrackRtEvent& e)                      : _track_event(e) {}\n    RtEvent(const ProcessorOperationRtEvent& e)         : _processor_operation_event(e) {}\n    RtEvent(const ProcessorReorderRtEvent& e)           : _processor_reorder_event(e) {}\n    RtEvent(const AsyncWorkRtEvent& e)                  : _async_work_event(e) {}\n    RtEvent(const AsyncWorkRtCompletionEvent& e)        : _async_work_completion_event(e) {}\n    RtEvent(const AudioConnectionRtEvent& e)            : _audio_connection_event(e) {}\n    RtEvent(const CvConnectionRtEvent& e)               : _cv_connection_event(e) {}\n    RtEvent(const GateConnectionRtEvent& e)             : _gate_connection_event(e) {}\n    RtEvent(const DataPayloadRtEvent& e)                : _data_payload_event(e) {}\n    RtEvent(const SynchronisationRtEvent& e)            : _synchronisation_event(e) {}\n    RtEvent(const TempoRtEvent& e)                      : _tempo_event(e) {}\n    RtEvent(const TimeSignatureRtEvent& e)              : _time_signature_event(e) {}\n    RtEvent(const PlayingModeRtEvent& e)                : _playing_mode_event(e) {}\n    RtEvent(const SyncModeRtEvent& e)                   : _sync_mode_event(e) {}\n    RtEvent(const ClipNotificationRtEvent& e)           : _clip_notification_event(e) {}\n    RtEvent(const DeleteDataRtEvent& e)                 : _delete_data_event(e) {}\n    RtEvent(const TimingTickRtEvent& e)                 : _timing_tick_event(e) {}\n    /* Data storage */\n    union\n    {\n        BaseRtEvent                   _base_event;\n        KeyboardRtEvent               _keyboard_event;\n        KeyboardCommonRtEvent         _keyboard_common_event;\n        WrappedMidiRtEvent            _wrapped_midi_event;\n        GateRtEvent                   _gate_event;\n        CvRtEvent                     _cv_event;\n        ParameterChangeRtEvent        _parameter_change_event;\n        PropertyChangeRtEvent         _property_change_event;\n        DataPropertyChangeRtEvent     _data_property_change_event;\n        ProcessorCommandRtEvent       _processor_command_event;\n        ProcessorStateRtEvent         _processor_state_event;\n        ProcessorNotifyRtEvent        _processor_notify_event;\n        ReturnableRtEvent             _returnable_event;\n        TrackRtEvent                  _track_event;\n        ProcessorOperationRtEvent     _processor_operation_event;\n        ProcessorReorderRtEvent       _processor_reorder_event;\n        AsyncWorkRtEvent              _async_work_event;\n        AsyncWorkRtCompletionEvent    _async_work_completion_event;\n        AudioConnectionRtEvent        _audio_connection_event;\n        CvConnectionRtEvent           _cv_connection_event;\n        GateConnectionRtEvent         _gate_connection_event;\n        DataPayloadRtEvent            _data_payload_event;\n        SynchronisationRtEvent        _synchronisation_event;\n        TempoRtEvent                  _tempo_event;\n        TimeSignatureRtEvent          _time_signature_event;\n        PlayingModeRtEvent            _playing_mode_event;\n        SyncModeRtEvent               _sync_mode_event;\n        ClipNotificationRtEvent       _clip_notification_event;\n        DeleteDataRtEvent             _delete_data_event;\n        TimingTickRtEvent             _timing_tick_event;\n    };\n};\n\n/* Compile time check that the event container fits withing given memory constraints and\n * they can be safely copied without side effects. Important for real-time performance*/\nstatic_assert(sizeof(RtEvent) == SUSHI_EVENT_CACHE_ALIGNMENT);\nstatic_assert(std::is_trivially_copyable<RtEvent>::value);\n\n/**\n * @brief Convenience function to encapsulate the logic to determine if it is a keyboard event\n *        and hence should be passed on to the next processor, or sent upwards.\n * @param event The event to test\n * @return true if the event is a keyboard event, false otherwise.\n */\ninline bool is_keyboard_event(const RtEvent& event)\n{\n    return event.type() >= RtEventType::NOTE_ON && event.type() <= RtEventType::WRAPPED_MIDI_EVENT;\n}\n\n/**\n * @brief Convenience function to encapsulate the logic to determine if an event is only for\n *        internal engine control or if it can be passed to processor instances\n * @param event The event to test\n * @return true if the event is only for internal engine use.\n */\ninline bool is_engine_control_event(const RtEvent& event)\n{\n    return event.type() >= RtEventType::TEMPO;\n}\n\n/**\n * @brief Convenience function to encapsulate the logic to determine if the event is\n *        returnable with a status code\n * @param event The event to test\n * @return true if the event can be converted to ReturnableRtEvent\n */\ninline bool is_returnable_event(const RtEvent& event)\n{\n    return event.type() >= RtEventType::INSERT_PROCESSOR && event.type() <= RtEventType::REMOVE_GATE_CONNECTION;\n}\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_RT_EVENTS_H\n"
  },
  {
    "path": "src/library/rt_event_fifo.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Fifo queues for RtEvents\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_REALTIME_FIFO_H\n#define SUSHI_REALTIME_FIFO_H\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"fifo/circularfifo_memory_relaxed_aquire_release.h\"\n#include \"library/simple_fifo.h\"\n#include \"library/rt_event.h\"\n#include \"library/rt_event_pipe.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_ALIGNMENT_PADDING\nELK_DISABLE_NON_TRIVIAL_DESTRUCTOR_NOT_VIRTUAL\n\nnamespace sushi::internal {\n\nconstexpr int MAX_EVENTS_IN_QUEUE = 1024;\n\n/**\n * @brief Wait free fifo queue for communication between rt and non-rt code\n */\nclass RtSafeRtEventFifo : public RtEventPipe\n{\npublic:\n    inline bool push(const RtEvent& event)\n    {\n        return _fifo.push(event);\n    }\n\n    inline bool pop(RtEvent& event)\n    {\n        return _fifo.pop(event);\n    }\n\n    inline bool empty()\n    {\n        return _fifo.wasEmpty();\n    }\n\n    void send_event(const RtEvent &event) override\n    {\n        push(event);\n    }\n\nprivate:\n    memory_relaxed_aquire_release::CircularFifo<RtEvent, MAX_EVENTS_IN_QUEUE> _fifo;\n};\n\n/**\n * @brief A simple RtEvent fifo implementation with internal storage that can be used\n *        internally when concurrent access from multiple threads is not neccesary\n * @tparam size Number of events to store in the queue\n */\ntemplate <size_t size = MAX_EVENTS_IN_QUEUE>\nclass RtEventFifo : public SimpleFifo<RtEvent, size>, public RtEventPipe\n{\npublic:\n    RtEventFifo() = default;\n    virtual ~RtEventFifo() = default;\n\n    void send_event(const RtEvent &event) override {SimpleFifo<RtEvent, size>::push(event);}\n};\n\n} //end namespace sushi::internal\n\nELK_POP_WARNING\n\n#endif // SUSHI_REALTIME_FIFO_H\n"
  },
  {
    "path": "src/library/rt_event_pipe.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Interface for events handlers/dispatchers/queues\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n *\n * Abstract interface for classes that receive and pass on events\n * to other. In some sense separate from Processor since this is\n * not intended to process or consume events, only pass them on.\n */\n\n#ifndef SUSHI_RT_EVENT_PIPE_H\n#define SUSHI_RT_EVENT_PIPE_H\n\n#include \"library/rt_event.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_TRIVIAL_DESTRUCTOR_NOT_VIRTUAL\n\nnamespace sushi::internal {\n\nclass RtEventPipe\n{\npublic:\n    virtual void send_event(const RtEvent& event) = 0;\n};\n\n} // end namespace sushi::internal\n\nELK_POP_WARNING\n\n#endif // SUSHI_RT_EVENT_PIPE_H\n"
  },
  {
    "path": "src/library/simple_fifo.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Simple, non-thread safe fifo queue for use internally in the engine when\n *        concurrent access is not necessary.\n *        Support for popping elements by value or reference, though in most cases\n *        the most efficient should be to iterate over the elements in place and\n *        then call clear()\n *        Capacity should ideally be a power of 2.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SIMPLE_FIFO_H\n#define SUSHI_SIMPLE_FIFO_H\n\n#include <array>\n#include <cassert>\n\nnamespace sushi::internal {\n\ntemplate<typename T, size_t storage_capacity>\nclass SimpleFifo\n{\npublic:\n    bool push(const T& element)\n    {\n        int new_tail = increment(_tail);\n        if (new_tail == _head)\n        {\n            return false; // Fifo is full\n        }\n        _data[_tail] = element;\n        _tail = new_tail;\n        return true;\n    }\n\n    bool pop(T& element)\n    {\n        if (empty())\n        {\n            return false; // Fifo is empty\n        }\n        element = _data[_head];\n        _head = increment(_head);\n        return true;\n    }\n\n    const T& pop()\n    {\n        // This should not be called on an empty queue\n        assert(empty() == false);\n        int old_head = _head;\n        _head = increment(_head);\n        return _data[old_head];\n    }\n\n    T operator [](int i) const\n    {\n        assert(static_cast<size_t>(i) < storage_capacity);\n        return _data[(_head + i) % storage_capacity];\n    }\n\n    T& operator [](int i)\n    {\n        assert(static_cast<size_t>(i) < storage_capacity);\n        return _data[(_head + i) % storage_capacity];\n    }\n\n    int size() const\n    {\n        if (_head <= _tail)\n        {\n            return _tail - _head;\n        }\n        return storage_capacity - _head + _tail;\n    }\n\n    // Actual capacity is 1 less than reserved storage, otherwise head == tail would indicate both full and empty\n    int capacity() const {return storage_capacity - 1;}\n\n    bool empty() const {return _head == _tail;}\n\n    void clear()\n    {\n        _head = 0;\n        _tail = 0;\n    }\n\nprivate:\n    inline int increment(int index) {return (index + 1) % storage_capacity;}\n\n    std::array<T, storage_capacity> _data;\n    // Elements are pushed at the tail and read from the head (head is first in the queue)\n    int _head{0};\n    int _tail{0};\n};\n\n} // end namespace sushi\n\n#endif // SUSHI_SIMPLE_FIFO_H\n"
  },
  {
    "path": "src/library/spinlock.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Basic spinlock implementation safe for xenomai use\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SPINLOCK_H\n#define SUSHI_SPINLOCK_H\n\n#include <atomic>\n\n#include \"sushi/constants.h\"\n\n// since std::hardware_destructive_interference_size is not yet supported in GCC 7\nconstexpr int ASSUMED_CACHE_LINE_SIZE = 64;\n\nnamespace sushi::internal {\n/**\n * @brief Simple rt-safe test-and-set spinlock\n */\nclass SpinLock\n{\npublic:\n    SpinLock() = default;\n\n    SUSHI_DECLARE_NON_COPYABLE(SpinLock);\n\n    void lock()\n    {\n        while (flag.load(std::memory_order_relaxed) == true)\n        {\n            /* Spin until flag is cleared. According to\n             * https://geidav.wordpress.com/2016/03/23/test-and-set-spinlocks/\n             * this is better as it causes fewer cache invalidations */\n        }\n        while (flag.exchange(true, std::memory_order_acquire))\n        {}\n    }\n\n    void unlock()\n    {\n        flag.store(false, std::memory_order_release);\n    }\n\nprivate:\n    alignas(ASSUMED_CACHE_LINE_SIZE) std::atomic_bool flag{false};\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_SPINLOCK_H\n"
  },
  {
    "path": "src/library/synchronised_fifo.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Generic, thread safe queue for use in non-rt threads\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SYNCHRONISED_FIFO_H\n#define SUSHI_SYNCHRONISED_FIFO_H\n\n#include <condition_variable>\n#include <chrono>\n#include <deque>\n\ntemplate <class T> class SynchronizedQueue\n{\npublic:\n    void push(T const& message)\n    {\n        std::lock_guard<std::mutex> lock(_queue_mutex);\n        _queue.push_front(message);\n        _notifier.notify_one();\n    }\n\n    void push(T&& message)\n    {\n        std::lock_guard<std::mutex> lock(_queue_mutex);\n        _queue.push_front(std::move(message));\n        _notifier.notify_one();\n    }\n\n    T pop()\n    {\n        std::lock_guard<std::mutex> lock(_queue_mutex);\n        T message = std::move(_queue.back());\n        _queue.pop_back();\n        return message;\n    }\n\n    void wait_for_data(const std::chrono::milliseconds& timeout)\n    {\n        if (_queue.empty())\n        {\n            std::unique_lock<std::mutex> lock(_wait_mutex);\n            _notifier.wait_for(lock, timeout);\n        }\n    }\n\n    bool empty()\n    {\n        return _queue.empty();\n    }\nprivate:\n    std::deque<T>           _queue;\n    std::mutex              _queue_mutex;\n    std::mutex              _wait_mutex;\n    std::condition_variable _notifier;\n};\n\n#endif // SUSHI_SYNCHRONISED_FIFO_H\n"
  },
  {
    "path": "src/library/vst2x/vst2x_host_callback.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Vst2 host callback implementation\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"twine/twine.h\"\n#include \"elklog/static_logger.h\"\n\n#include \"vst2x_host_callback.h\"\n\n#include \"vst2x_wrapper.h\"\n\nnamespace sushi::internal::vst2 {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"vst2\");\n\nVstIntPtr VSTCALLBACK host_callback(AEffect* effect, VstInt32 opcode, VstInt32 index,\n                                    [[maybe_unused]] VstIntPtr value,\n                                    [[maybe_unused]] void* ptr, float opt)\n{\n    VstIntPtr result = 0;\n\n    switch (opcode)\n    {\n        case audioMasterAutomate:\n        {\n            auto wrapper_instance = reinterpret_cast<Vst2xWrapper*>(effect->user);\n            if (wrapper_instance == nullptr)\n            {\n                return 0; // Plugins could call this during initialisation, before the wrapper has finished construction\n            }\n            if (twine::is_current_thread_realtime())\n            {\n                wrapper_instance->notify_parameter_change_rt(index, opt);\n            }\n            else\n            {\n                wrapper_instance->notify_parameter_change(index, opt);\n                ELKLOG_LOG_DEBUG(\"Plugin {} sending parameter change notification: param: {}, value: {}\",\n                                wrapper_instance->name(), index, opt);\n            }\n            break;\n        }\n\n        case audioMasterVersion:\n        {\n            result = kVstVersion;\n            break;\n        }\n\n        case audioMasterGetTime:\n        {\n            // Pass a pointer to a VstTimeInfo populated with updated data to the plugin\n            auto wrapper_instance = reinterpret_cast<Vst2xWrapper*>(effect->user);\n            if (wrapper_instance == nullptr)\n            {\n                return 0; // Plugins could call this during initialisation, before the wrapper has finished construction\n            }\n            result = reinterpret_cast<VstIntPtr>(wrapper_instance->time_info());\n            break;\n        }\n\n        case audioMasterProcessEvents:\n        {\n            auto wrapper_instance = reinterpret_cast<Vst2xWrapper*>(effect->user);\n            if (wrapper_instance == nullptr)\n            {\n                return 0; // Plugins could call this during initialisation, before the wrapper has finished construction\n            }\n            auto events = reinterpret_cast<VstEvents*>(ptr);\n            for (int i = 0; i < events->numEvents; ++i)\n            {\n                wrapper_instance->output_vst_event(events->events[i]);\n            }\n            break;\n        }\n\n        default:\n            break;\n    }\n\n    return result;\n\n}\n\n} // end namespace sushi::internal::vst2\n"
  },
  {
    "path": "src/library/vst2x/vst2x_host_callback.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Vst2 host callback implementation\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_VST2X_HOST_CALLBACK_H\n#define SUSHI_VST2X_HOST_CALLBACK_H\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_UNUSED_PARAMETER\n#define VST_FORCE_DEPRECATED 0\n#include \"aeffectx.h\"\nELK_POP_WARNING\n\nnamespace sushi::internal::vst2 {\n\ntypedef AEffect *(*plugin_entry_proc)(audioMasterCallback host);\n\nextern VstIntPtr VSTCALLBACK host_callback(AEffect* effect,\n                                           VstInt32 opcode, VstInt32 index,\n                                           VstIntPtr value, void* ptr, float opt);\n\n} // end namespace sushi::internal::vst2\n\n#endif // SUSHI_VST2X_HOST_CALLBACK_H\n"
  },
  {
    "path": "src/library/vst2x/vst2x_midi_event_fifo.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Circular Buffer FIFO for storing events to be passed\n *        to VsT API call processEvents(VstEvents* events).\n * @warning Not thread-safe! It's ok with the current architecture where\n *          Processor::process_event(..) is called in the real-time thread\n *          before processing.\n *\n *          FIFO policy is a circular buffer which just overwrites old events,\n *          signaling the producer in case of overflow.\n *\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_VST2X_MIDI_EVENT_FIFO_H\n#define SUSHI_VST2X_MIDI_EVENT_FIFO_H\n\n#include <cmath>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_UNUSED_PARAMETER\n#define VST_FORCE_DEPRECATED 0\n#include \"aeffectx.h\"\nELK_POP_WARNING\n\n#include \"sushi/constants.h\"\n\n#include \"library/rt_event.h\"\n#include \"library/midi_decoder.h\"\n#include \"library/midi_encoder.h\"\n\nnamespace sushi::internal::vst2 {\n\ntemplate<int capacity>\nclass Vst2xMidiEventFIFO\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(Vst2xMidiEventFIFO)\n    /**\n     *  @brief Allocate VstMidiEvent buffer and prepare VstEvents* pointers\n     */\n    Vst2xMidiEventFIFO()\n    {\n        _midi_data = new VstMidiEvent[capacity];\n        _vst_events = new VstEventsExtended();\n        _vst_events->numEvents = 0;\n        _vst_events->reserved = 0;\n\n        for (int i=0; i<capacity; i++)\n        {\n            auto midi_ev_p = &_midi_data[i];\n            midi_ev_p->type = kVstMidiType;\n            midi_ev_p->byteSize = sizeof(VstMidiEvent);\n            midi_ev_p->flags = kVstMidiEventIsRealtime;\n            _vst_events->events[i] = reinterpret_cast<VstEvent*>(midi_ev_p);\n        }\n    }\n\n    ~Vst2xMidiEventFIFO()\n    {\n        delete[] _midi_data;\n        delete   _vst_events;\n    }\n\n    /**\n     *  @brief Push an element to the FIFO.\n     *  @return false if overflow happened during the operation\n     */\n    bool push(RtEvent event)\n    {\n        bool res = !_limit_reached;\n        _fill_vst_event(_write_idx, event);\n\n        _write_idx++;\n        if (!_limit_reached)\n        {\n            _size++;\n        }\n        if (_write_idx == capacity)\n        {\n            // Reached end of buffer: wrap pointer and next call will signal overflow\n            _write_idx = 0;\n            _limit_reached = true;\n        }\n\n        return res;\n    }\n\n    /**\n     * @brief Return pointer to stored VstEvents*\n     *        You should process _all_ the returned values before any subsequent\n     *        call to push(), as the internal buffer is flushed after the call.\n     * @return Pointer to be passed directly to processEvents() in the real-time thread\n     */\n    VstEvents* flush()\n    {\n        _vst_events->numEvents = _size;\n\n        // reset internal buffers\n        _size = 0;\n        _write_idx = 0;\n        _limit_reached = false;\n\n        return reinterpret_cast<VstEvents*>(_vst_events);\n    }\n\nprivate:\n    /**\n     * The original VstEvents struct declares events[2] to mark a variable-size\n     * array. With this private struct we can avoid ugly C-style reallocations.\n     */\n    struct VstEventsExtended\n    {\n        VstInt32 numEvents;\n        VstIntPtr reserved;\n        VstEvent* events[capacity];\n    };\n\n    /**\n    * @brief Helper to initialize VstMidiEvent inside the buffer from Event\n    *\n    */\n    void _fill_vst_event(const int idx, RtEvent event)\n    {\n        auto midi_ev_p = &_midi_data[idx];\n        midi_ev_p->deltaFrames = static_cast<VstInt32>(event.sample_offset());\n        MidiDataByte midi_data;\n\n        switch (event.type())\n        {\n            case RtEventType::NOTE_ON:\n            {\n                auto typed_event = event.keyboard_event();\n                midi_data = midi::encode_note_on(typed_event->channel(), typed_event->note(), typed_event->velocity());\n                break;\n            }\n            case RtEventType::NOTE_OFF:\n            {\n                auto typed_event = event.keyboard_event();\n                midi_data = midi::encode_note_off(typed_event->channel(), typed_event->note(), typed_event->velocity());\n                // For some reason, VstMidiEvent has an additional explicit field noteOffVelocity\n                midi_ev_p->noteOffVelocity = midi_data[2];\n                break;\n            }\n            case RtEventType::NOTE_AFTERTOUCH:\n            {\n                auto typed_event = event.keyboard_event();\n                midi_data = midi::encode_poly_key_pressure(typed_event->channel(), typed_event->note(), typed_event->velocity());\n                break;\n            }\n            case RtEventType::PITCH_BEND:\n            {\n                auto typed_event = event.keyboard_common_event();\n                midi_data = midi::encode_pitch_bend(typed_event->channel(), typed_event->value());\n                break;\n            }\n            case RtEventType::AFTERTOUCH:\n            {\n                auto typed_event = event.keyboard_common_event();\n                midi_data = midi::encode_channel_pressure(typed_event->channel(), typed_event->value());\n                break;\n            }\n            case RtEventType::MODULATION:\n            {\n                auto typed_event = event.keyboard_common_event();\n                midi_data = midi::encode_control_change(typed_event->channel(), midi::MOD_WHEEL_CONTROLLER_NO, typed_event->value());\n                break;\n            }\n            case RtEventType::WRAPPED_MIDI_EVENT:\n            {\n                auto typed_event = event.wrapped_midi_event();\n                midi_data = typed_event->midi_data();\n                break;\n            }\n            default:\n                return;\n        }\n        std::copy(midi_data.begin(), midi_data.end(), midi_ev_p->midiData);\n    }\n\n    int _size{0};\n    int _write_idx{0};\n    bool _limit_reached{false};\n\n    VstMidiEvent* _midi_data;\n    VstEventsExtended* _vst_events;\n};\n\n} // end namespace sushi::internal::vst2\n\n#endif // SUSHI_VST2X_MIDI_EVENT_FIFO_H\n"
  },
  {
    "path": "src/library/vst2x/vst2x_plugin_loader.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utilities for loading plugins stored in dynamic libraries\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n *\n * Parts taken and/or adapted from:\n * MrsWatson - https://github.com/teragonaudio/MrsWatson\n *\n * Original copyright notice with BSD license:\n * Copyright (c) 2013 Teragon Audio. All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are met:\n *\n * * Redistributions of source code must retain the above copyright notice,\n *   this list of conditions and the following disclaimer.\n * * Redistributions in binary form must reproduce the above copyright notice,\n *   this list of conditions and the following disclaimer in the documentation\n *   and/or other materials provided with the distribution.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\n * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n * POSSIBILITY OF SUCH DAMAGE.\n */\n\n#include <filesystem>\n#include <cassert>\n\n#ifdef __APPLE__\n#include <Corefoundation/Corefoundation.h>\n#elif defined(_MSC_VER)\n#define NOMINMAX 1\n#include <Windows.h>\n#endif\n\n#include \"elklog/static_logger.h\"\n\n#include \"vst2x_plugin_loader.h\"\n#include \"vst2x_host_callback.h\"\n\nnamespace sushi::internal::vst2 {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"vst2\");\n\n#if defined(__linux__)\n\nLibraryHandle PluginLoader::get_library_handle_for_plugin(const std::string& plugin_absolute_path)\n{\n    if (! std::filesystem::exists(plugin_absolute_path))\n    {\n        ELKLOG_LOG_ERROR(\"Plugin path not found: {}\", plugin_absolute_path);\n        return nullptr;\n    }\n    void *libraryHandle = dlopen(plugin_absolute_path.c_str(), RTLD_NOW | RTLD_LOCAL);\n\n    if (libraryHandle == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Could not open library, {}\", dlerror());\n        return nullptr;\n    }\n\n    return libraryHandle;\n}\n\nAEffect* PluginLoader::load_plugin(LibraryHandle library_handle)\n{\n    // Somewhat cheap hack to avoid a tricky compiler warning. Casting from void*\n    // to a proper function pointer will cause GCC to warn that \"ISO C++ forbids\n    // casting between pointer-to-function and pointer-to-object\". Here, we\n    // represent both types in a union and use the correct one in the given\n    // context, thus avoiding the need to cast anything.  See also:\n    // http://stackoverflow.com/a/2742234/14302\n    union\n    {\n        plugin_entry_proc entryPointFuncPtr;\n        void *entryPointVoidPtr;\n    } entryPoint;\n\n    entryPoint.entryPointVoidPtr = dlsym(library_handle, \"VSTPluginMain\");\n\n    if (entryPoint.entryPointVoidPtr == nullptr)\n    {\n        entryPoint.entryPointVoidPtr = dlsym(library_handle, \"main\");\n        if (entryPoint.entryPointVoidPtr == nullptr)\n        {\n              ELKLOG_LOG_ERROR(\"Couldn't get a pointer to plugin's main()\");\n              return nullptr;\n        }\n    }\n\n    plugin_entry_proc mainEntryPoint = entryPoint.entryPointFuncPtr;\n    AEffect *plugin = mainEntryPoint(host_callback);\n    return plugin;\n}\n\nvoid PluginLoader::close_library_handle(LibraryHandle library_handle)\n{\n    if (dlclose(library_handle) != 0)\n    {\n        ELKLOG_LOG_WARNING(\"Could not safely close plugin, possible resource leak\");\n    }\n}\n\n#elif defined(__APPLE__)\nLibraryHandle PluginLoader::get_library_handle_for_plugin(const std::string& plugin_absolute_path)\n{\n    CFURLRef bundle_url;\n    CFBundleRef bundle_handle;\n    if (! std::filesystem::exists(plugin_absolute_path))\n    {\n        ELKLOG_LOG_ERROR(\"Plugin path not found: {}\", plugin_absolute_path);\n        return nullptr;\n    }\n    bundle_url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault,\n                                                        (const UInt8*) plugin_absolute_path.c_str(),\n                                                        plugin_absolute_path.size(),\n                                                        true );\n    bundle_handle = CFBundleCreate(kCFAllocatorDefault, bundle_url);\n    CFRelease(bundle_url);\n\n    if (bundle_handle == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Could not open bundle\");\n        return nullptr;\n    }\n    return (LibraryHandle)bundle_handle;\n}\n\nAEffect* PluginLoader::load_plugin(LibraryHandle library_handle)\n{\n    // Somewhat cheap hack to avoid a tricky compiler warning. Casting from void*\n    // to a proper function pointer will cause GCC to warn that \"ISO C++ forbids\n    // casting between pointer-to-function and pointer-to-object\". Here, we\n    // represent both types in a union and use the correct one in the given\n    // context, thus avoiding the need to cast anything.  See also:\n    // http://stackoverflow.com/a/2742234/14302\n    union\n    {\n        plugin_entry_proc entryPointFuncPtr;\n        void *entryPointVoidPtr;\n    } entryPoint;\n\n    entryPoint.entryPointVoidPtr = CFBundleGetFunctionPointerForName ((CFBundleRef)library_handle, CFSTR(\"main_macho\"));\n\n    if (entryPoint.entryPointVoidPtr == nullptr)\n    {\n        entryPoint.entryPointVoidPtr = CFBundleGetFunctionPointerForName ((CFBundleRef)library_handle, CFSTR(\"VSTPluginMain\"));\n        if (entryPoint.entryPointVoidPtr == nullptr)\n        {\n            entryPoint.entryPointVoidPtr = CFBundleGetFunctionPointerForName ((CFBundleRef)library_handle, CFSTR(\"main\"));\n            if (entryPoint.entryPointVoidPtr == nullptr)\n            {\n              ELKLOG_LOG_ERROR(\"Couldn't get a pointer to plugin's main()\");\n              return nullptr;\n            }\n        }\n    }\n\n    plugin_entry_proc mainEntryPoint = entryPoint.entryPointFuncPtr;\n    AEffect *plugin = mainEntryPoint(host_callback);\n    return plugin;\n}\n\nvoid PluginLoader::close_library_handle(LibraryHandle library_handle)\n{\n    // Not sure if we should really need to unload the executable manually.\n    // Apples docs say that as long you match the number of \"CFBundleCreate...\" with\n    // \"CFRelease\" we should be fine. Also, it apparently only loads bundle once.\n    assert (library_handle);\n    if (CFGetRetainCount(library_handle) == 1)\n    {\n        CFBundleUnloadExecutable((CFBundleRef)library_handle);\n        if (CFBundleIsExecutableLoaded((CFBundleRef)library_handle))\n        {\n            ELKLOG_LOG_WARNING(\"Could not safely close plugin, possible resource leak\");\n        }\n    }\n\n    if (CFGetRetainCount(library_handle) > 0)\n    {\n        CFRelease(library_handle);\n    }\n}\n#elif defined(_MSC_VER)\nLibraryHandle PluginLoader::get_library_handle_for_plugin(const std::string& plugin_absolute_path)\n{\n    if (!std::filesystem::exists(plugin_absolute_path))\n    {\n        ELKLOG_LOG_ERROR(\"Plugin path not found: {}\", plugin_absolute_path);\n        return nullptr;\n    }\n    void* libraryHandle = LoadLibrary(plugin_absolute_path.c_str());\n\n    if (libraryHandle == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"Could not open library, {}\", GetLastError());\n        return nullptr;\n    }\n\n    return libraryHandle;\n}\n\nAEffect* PluginLoader::load_plugin(LibraryHandle library_handle)\n{\n    // Somewhat cheap hack to avoid a tricky compiler warning. Casting from void*\n    // to a proper function pointer will cause GCC to warn that \"ISO C++ forbids\n    // casting between pointer-to-function and pointer-to-object\". Here, we\n    // represent both types in a union and use the correct one in the given\n    // context, thus avoiding the need to cast anything.  See also:\n    // http://stackoverflow.com/a/2742234/14302\n    union\n    {\n        plugin_entry_proc entryPointFuncPtr;\n        void* entryPointVoidPtr;\n    } entryPoint;\n\n    entryPoint.entryPointVoidPtr = GetProcAddress((HMODULE) library_handle, \"VSTPluginMain\");\n\n    if (entryPoint.entryPointVoidPtr == nullptr)\n    {\n        entryPoint.entryPointVoidPtr = GetProcAddress((HMODULE) library_handle, \"main\");\n        if (entryPoint.entryPointVoidPtr == nullptr)\n        {\n            ELKLOG_LOG_ERROR(\"Couldn't get a pointer to plugin's main()\");\n            return nullptr;\n        }\n    }\n\n    plugin_entry_proc mainEntryPoint = entryPoint.entryPointFuncPtr;\n    AEffect* plugin = mainEntryPoint(host_callback);\n    return plugin;\n}\n\nvoid PluginLoader::close_library_handle(LibraryHandle library_handle)\n{\n    if (library_handle != nullptr)\n    {\n        FreeLibrary(HMODULE(library_handle));\n    }\n}\n#endif\n\n} // end namespace sushi::internal::vst2\n\n"
  },
  {
    "path": "src/library/vst2x/vst2x_plugin_loader.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utilities for loading plugins stored in dynamic libraries\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n *\n * Parts taken and/or adapted from:\n * MrsWatson - https://github.com/teragonaudio/MrsWatson\n *\n * Original copyright notice with BSD license:\n * Copyright (c) 2013 Teragon Audio. All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are met:\n *\n * * Redistributions of source code must retain the above copyright notice,\n *   this list of conditions and the following disclaimer.\n * * Redistributions in binary form must reproduce the above copyright notice,\n *   this list of conditions and the following disclaimer in the documentation\n *   and/or other materials provided with the distribution.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\n * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n * POSSIBILITY OF SUCH DAMAGE.\n */\n\n#ifndef SUSHI_VST2X_PLUGIN_LOADER_H\n#define SUSHI_VST2X_PLUGIN_LOADER_H\n\n#include <string>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_UNUSED_PARAMETER\n#define VST_FORCE_DEPRECATED 0\n#include \"aeffectx.h\"\nELK_POP_WARNING\n\n#ifndef _MSC_VER\n#include <dlfcn.h>\n#endif\n\nnamespace sushi::internal::vst2 {\n\nusing LibraryHandle = void*;\n\n// TODO:\n//      this class is stateless atm (basically a namespace),\n//      but it should probably grow into the access point to plugins stored in the\n//      system, with features like directory scanning, caching, easier handling\n//      of library handles, etc.\n\nclass PluginLoader\n{\npublic:\n    static LibraryHandle get_library_handle_for_plugin(const std::string& plugin_absolute_path);\n\n    static AEffect* load_plugin(LibraryHandle library_handle);\n\n    static void close_library_handle(LibraryHandle library_handle);\n};\n\n} // end namespace sushi::internal::vst2\n\n#endif // SUSHI_VST2X_PLUGIN_LOADER_H\n"
  },
  {
    "path": "src/library/vst2x/vst2x_processor_factory.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Factory implementation for VST2 processors.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"vst2x_processor_factory.h\"\n\n#ifdef SUSHI_BUILD_WITH_VST2\n#include \"library/vst2x/vst2x_wrapper.h\"\n#endif\n\nnamespace sushi::internal::vst2 {\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_UNUSED_CONST_VARIABLE\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"Vst2\");\nELK_POP_WARNING\n\n#ifdef SUSHI_BUILD_WITH_VST2\n\nstd::pair<ProcessorReturnCode, std::shared_ptr<Processor>> Vst2xProcessorFactory::new_instance(const PluginInfo& plugin_info,\n                                                                                               HostControl& host_control,\n                                                                                               float sample_rate)\n{\n    auto processor = std::make_shared<Vst2xWrapper>(host_control, plugin_info.path);\n    auto processor_status = processor->init(sample_rate);\n    return {processor_status, processor};\n}\n\n#else //SUSHI_BUILD_WITH_VST2\n\nstd::pair<ProcessorReturnCode, std::shared_ptr<Processor>> Vst2xProcessorFactory::new_instance([[maybe_unused]] const PluginInfo& plugin_info,\n                                                                                               [[maybe_unused]] HostControl& host_control,\n                                                                                               [[maybe_unused]] float sample_rate)\n\n{\n    ELKLOG_LOG_ERROR(\"Sushi was not built with support for VST2 plugins\");\n    return {ProcessorReturnCode::UNSUPPORTED_OPERATION, nullptr};\n}\n\n#endif // SUSHI_BUILD_WITH_VST2\n\n} // end namespace sushi::internal::vst2\n"
  },
  {
    "path": "src/library/vst2x/vst2x_processor_factory.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Factory for VST2 processors.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_VST2X_PROCESSOR_FACTORY_H\n#define SUSHI_VST2X_PROCESSOR_FACTORY_H\n\n#include \"library/base_processor_factory.h\"\n\nnamespace sushi::internal::vst2 {\n\nclass Vst2xProcessorFactory : public BaseProcessorFactory\n{\npublic:\n    std::pair<ProcessorReturnCode, std::shared_ptr<Processor>> new_instance(const PluginInfo& plugin_info,\n                                                                            HostControl& host_control,\n                                                                            float sample_rate) override;\n};\n\n} // end namespace sushi::internal::vst2\n\n#endif // SUSHI_VST2X_PROCESSOR_FACTORY_H\n"
  },
  {
    "path": "src/library/vst2x/vst2x_wrapper.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Wrapper for VST 2.x plugins.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"twine/twine.h\"\n#include \"elklog/static_logger.h\"\n\n#include \"vst2x_wrapper.h\"\n\n#include \"library/midi_decoder.h\"\n\nnamespace {\n\nconstexpr int VST_STRING_BUFFER_SIZE = 256;\nconstexpr int SINGLE_PROGRAM = 1;\nchar canDoBypass[] = \"bypass\";\n\n} // anonymous namespace\n\nnamespace sushi::internal::vst2 {\n\nconstexpr int SUSHI_HOST_TIME_CAPABILITIES = kVstNanosValid | kVstPpqPosValid | kVstTempoValid |\n                                             kVstBarsValid | kVstTimeSigValid;\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"vst2\");\n\nVst2xWrapper::~Vst2xWrapper()\n{\n    ELKLOG_LOG_DEBUG(\"Unloading plugin {}\", this->name());\n    _cleanup();\n}\n\nProcessorReturnCode Vst2xWrapper::init(float sample_rate)\n{\n    // TODO: sanity checks on sample_rate,\n    //       but these can probably better be handled on Processor::init()\n    _sample_rate = sample_rate;\n\n    // Load shared library and VsT struct\n    _library_handle = PluginLoader::get_library_handle_for_plugin(_host_control.to_absolute_path(_plugin_path));\n    if (_library_handle == nullptr)\n    {\n        _cleanup();\n        return ProcessorReturnCode::SHARED_LIBRARY_OPENING_ERROR;\n    }\n    _plugin_handle = PluginLoader::load_plugin(_library_handle);\n    if (_plugin_handle == nullptr)\n    {\n        _cleanup();\n        return ProcessorReturnCode::PLUGIN_ENTRY_POINT_NOT_FOUND;\n    }\n\n    // Check plugin's magic number\n    // If incorrect, then the file either was not loaded properly, is not a\n    // real VST2 plugin, or is otherwise corrupt.\n    if (_plugin_handle->magic != kEffectMagic)\n    {\n        _cleanup();\n        return ProcessorReturnCode::PLUGIN_LOAD_ERROR;\n    }\n\n    // Set Processor's name and label (using VST's ProductString)\n    char effect_name[VST_STRING_BUFFER_SIZE] = {0};\n    char product_string[VST_STRING_BUFFER_SIZE] = {0};\n\n    _vst_dispatcher(effGetEffectName, 0, 0, effect_name, 0);\n    _vst_dispatcher(effGetProductString, 0, 0, product_string, 0);\n    set_name(std::string(&effect_name[0]));\n    set_label(std::string(&product_string[0]));\n\n    // Get plugin can do:s\n    int enabled = _vst_dispatcher(effCanDo, 0, 0, canDoBypass, 0);\n    _can_do_soft_bypass = (enabled == 1);\n    _number_of_programs = _plugin_handle->numPrograms;\n    ELKLOG_LOG_INFO_IF(enabled, \"Plugin supports soft bypass\");\n\n    _has_binary_programs = _plugin_handle->flags &= effFlagsProgramChunks;\n\n    // Channel setup\n    _max_input_channels = std::min(_plugin_handle->numInputs, VST_WRAPPER_MAX_N_CHANNELS);\n    _max_output_channels = std::min(_plugin_handle->numOutputs, VST_WRAPPER_MAX_N_CHANNELS);\n\n    // Initialize internal plugin\n    _vst_dispatcher(effOpen, 0, 0, nullptr, 0);\n    _vst_dispatcher(effSetSampleRate, 0, 0, nullptr, _sample_rate);\n    _vst_dispatcher(effSetBlockSize, 0, AUDIO_CHUNK_SIZE, nullptr, 0);\n\n    // Register internal parameters\n    if (!_register_parameters())\n    {\n        _cleanup();\n        return ProcessorReturnCode::PARAMETER_ERROR;\n    }\n\n    // Register yourself\n    _plugin_handle->user = this;\n    return ProcessorReturnCode::OK;\n}\n\nvoid Vst2xWrapper::configure(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    bool reset_enabled = enabled();\n    if (reset_enabled)\n    {\n        set_enabled(false);\n    }\n    _vst_dispatcher(effSetSampleRate, 0, 0, nullptr, _sample_rate);\n    if (reset_enabled)\n    {\n        set_enabled(true);\n    }\n}\n\nvoid Vst2xWrapper::set_channels(int inputs, int outputs)\n{\n    Processor::set_channels(inputs, outputs);\n    [[maybe_unused]] bool valid_arr = _update_speaker_arrangements(inputs, outputs);\n    ELKLOG_LOG_WARNING_IF(!valid_arr, \"Failed to set a valid speaker arrangement\")\n}\n\nvoid Vst2xWrapper::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    if (enabled)\n    {\n        _vst_dispatcher(effMainsChanged, 0, 1, nullptr, 0.0f);\n        _vst_dispatcher(effStartProcess, 0, 0, nullptr, 0.0f);\n    }\n    else\n    {\n        _vst_dispatcher(effMainsChanged, 0, 0, nullptr, 0.0f);\n        _vst_dispatcher(effStopProcess, 0, 0, nullptr, 0.0f);\n    }\n}\n\nvoid Vst2xWrapper::set_bypassed(bool bypassed)\n{\n    assert(twine::is_current_thread_realtime() == false);\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nstd::pair<ProcessorReturnCode, float> Vst2xWrapper::parameter_value(ObjectId parameter_id) const\n{\n    if (static_cast<int>(parameter_id) < _plugin_handle->numParams)\n    {\n        float value  = _plugin_handle->getParameter(_plugin_handle, static_cast<VstInt32>(parameter_id));\n        return {ProcessorReturnCode::OK, value};\n    }\n\n    return {ProcessorReturnCode::PARAMETER_NOT_FOUND, 0.0f};\n}\n\nstd::pair<ProcessorReturnCode, float> Vst2xWrapper::parameter_value_in_domain(ObjectId parameter_id) const\n{\n    return this->parameter_value(parameter_id);\n}\n\nstd::pair<ProcessorReturnCode, std::string> Vst2xWrapper::parameter_value_formatted(ObjectId parameter_id) const\n{\n    if (static_cast<int>(parameter_id) < _plugin_handle->numParams)\n    {\n        // Many plugins don't respect the kVstMaxParamStrLen limit, so better use a larger buffer\n        char buffer[VST_STRING_BUFFER_SIZE] = \"\";\n        _vst_dispatcher(effGetParamDisplay, static_cast<VstInt32>(parameter_id), 0, buffer, 0);\n        buffer[VST_STRING_BUFFER_SIZE-1] = 0;\n        return {ProcessorReturnCode::OK, buffer};\n    }\n    return {ProcessorReturnCode::PARAMETER_NOT_FOUND, \"\"};\n}\n\nint Vst2xWrapper::current_program() const\n{\n    if (this->supports_programs())\n    {\n        return _vst_dispatcher(effGetProgram, 0, 0, nullptr, 0);\n    }\n    return 0;\n}\n\nstd::string Vst2xWrapper::current_program_name() const\n{\n    if (this->supports_programs())\n    {\n        char buffer[VST_STRING_BUFFER_SIZE] = \"\";\n        _vst_dispatcher(effGetProgramName, 0, 0, buffer, 0);\n        buffer[VST_STRING_BUFFER_SIZE-1] = 0;\n        return {buffer};\n    }\n    return {};\n}\n\nstd::pair<ProcessorReturnCode, std::string> Vst2xWrapper::program_name(int program) const\n{\n    if (this->supports_programs())\n    {\n        char buffer[VST_STRING_BUFFER_SIZE] = \"\";\n        auto success = _vst_dispatcher(effGetProgramNameIndexed, program, 0, buffer, 0);\n        buffer[VST_STRING_BUFFER_SIZE - 1] = 0;\n        return {success ? ProcessorReturnCode::OK : ProcessorReturnCode::PARAMETER_NOT_FOUND, buffer};\n    }\n    return {ProcessorReturnCode::UNSUPPORTED_OPERATION, \"\"};\n}\n\nstd::pair<ProcessorReturnCode, std::vector<std::string>> Vst2xWrapper::all_program_names() const\n{\n    if (!this->supports_programs())\n    {\n        return {ProcessorReturnCode::UNSUPPORTED_OPERATION, std::vector<std::string>()};\n    }\n    std::vector<std::string> programs;\n    for (int i = 0; i < _number_of_programs; ++i)\n    {\n        char buffer[VST_STRING_BUFFER_SIZE] = \"\";\n        _vst_dispatcher(effGetProgramNameIndexed, i, 0, buffer, 0);\n        buffer[VST_STRING_BUFFER_SIZE-1] = 0;\n        programs.emplace_back(buffer);\n    }\n    return {ProcessorReturnCode::OK, programs};\n}\n\nProcessorReturnCode Vst2xWrapper::set_program(int program)\n{\n    if (this->supports_programs() && program < _number_of_programs)\n    {\n        _vst_dispatcher(effBeginSetProgram, 0, 0, nullptr, 0);\n        /* Vst2 lacks a mechanism for signaling that the program change was successful */\n        _vst_dispatcher(effSetProgram, 0, program, nullptr, 0);\n        _vst_dispatcher(effEndSetProgram, 0, 0, nullptr, 0);\n\n        _host_control.post_event(std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_UPDATED,\n                                                                               this->id(), 0, IMMEDIATE_PROCESS));\n        return ProcessorReturnCode::OK;\n    }\n    return ProcessorReturnCode::UNSUPPORTED_OPERATION;\n}\n\nvoid Vst2xWrapper::_cleanup()\n{\n    if (_plugin_handle != nullptr)\n    {\n        // Tell plugin to stop and shutdown\n        set_enabled(false);\n        _vst_dispatcher(effClose, 0, 0, nullptr, 0);\n        _plugin_handle = nullptr;\n    }\n    if (_library_handle != nullptr)\n    {\n        PluginLoader::close_library_handle(_library_handle);\n        _library_handle = nullptr;\n    }\n}\n\nbool Vst2xWrapper::_register_parameters()\n{\n    char param_name[VST_STRING_BUFFER_SIZE] = {0};\n    char param_unit[VST_STRING_BUFFER_SIZE] = {0};\n\n    VstInt32 idx = 0;\n    bool param_inserted_ok = true;\n    while (param_inserted_ok && (idx < _plugin_handle->numParams) )\n    {\n        _vst_dispatcher(effGetParamName, idx, 0, param_name, 0);\n        _vst_dispatcher(effGetParamLabel, idx, 0, param_unit, 0);\n        param_name[VST_STRING_BUFFER_SIZE-1] = 0;\n        param_unit[VST_STRING_BUFFER_SIZE-1] = 0;\n        auto unique_name = _make_unique_parameter_name(param_name);\n\n        param_inserted_ok = register_parameter(new FloatParameterDescriptor(unique_name,\n                                                                            param_name,\n                                                                            param_unit,\n                                                                            0,\n                                                                            1,\n                                                                            Direction::AUTOMATABLE,\n                                                                            nullptr));\n        if (param_inserted_ok)\n        {\n            ELKLOG_LOG_DEBUG(\"Plugin: {}, registered param: {}\", name(), param_name);\n        }\n        else\n        {\n            ELKLOG_LOG_ERROR(\"Plugin: {}, Error while registering param: {}\", name(), param_name);\n        }\n        idx++;\n    }\n\n    return param_inserted_ok;\n}\n\nvoid Vst2xWrapper::process_event(const RtEvent& event)\n{\n\n    switch(event.type())\n    {\n        case RtEventType::FLOAT_PARAMETER_CHANGE:\n        {\n            auto typed_event = event.parameter_change_event();\n            auto id = typed_event->param_id();\n            assert(static_cast<VstInt32>(id) < _plugin_handle->numParams);\n            _plugin_handle->setParameter(_plugin_handle, static_cast<VstInt32>(id), typed_event->value());\n            break;\n        }\n\n        case RtEventType::NOTE_ON:\n        case RtEventType::NOTE_OFF:\n        case RtEventType::NOTE_AFTERTOUCH:\n        case RtEventType::PITCH_BEND:\n        case RtEventType::AFTERTOUCH:\n        case RtEventType::MODULATION:\n        case RtEventType::WRAPPED_MIDI_EVENT:\n        {\n            if (_vst_midi_events_fifo.push(event) == false)\n            {\n                ELKLOG_LOG_WARNING(\"Plugin: {}, MIDI queue Overflow!\", name());\n            }\n            break;\n        }\n\n        case RtEventType::SET_BYPASS:\n        {\n            bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n            _set_bypass_rt(bypassed);\n            break;\n        }\n\n        case RtEventType::SET_STATE:\n        {\n            auto state = event.processor_state_event()->state();\n            _set_state_rt(state);\n            break;\n        }\n\n        default:\n            break;\n    }\n}\n\nvoid Vst2xWrapper::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    if (_can_do_soft_bypass == false && _bypass_manager.should_process() == false)\n    {\n        bypass_process(in_buffer, out_buffer);\n        _vst_midi_events_fifo.flush();\n    }\n    else\n    {\n        _vst_dispatcher(effProcessEvents, 0, 0, _vst_midi_events_fifo.flush(), 0.0f);\n        _map_audio_buffers(in_buffer, out_buffer);\n        _plugin_handle->processReplacing(_plugin_handle, _process_inputs, _process_outputs, AUDIO_CHUNK_SIZE);\n        if (_can_do_soft_bypass == false && _bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer, _current_input_channels, _current_output_channels);\n        }\n    }\n}\n\nvoid Vst2xWrapper::notify_parameter_change_rt(VstInt32 parameter_index, float value)\n{\n    /* The default Vst2.4 implementation calls set_parameter() in set_parameter_automated()\n     * so the plugin is already aware of the change, we just need to send a notification\n     * to the non-rt part */\n    if (parameter_index > this->parameter_count())\n    {\n        return;\n    }\n    if (maybe_output_cv_value(parameter_index, value) == false)\n    {\n        auto e = RtEvent::make_parameter_change_event(this->id(), 0, static_cast<ObjectId>(parameter_index), value);\n        output_event(e);\n    }\n}\n\nvoid Vst2xWrapper::notify_parameter_change(VstInt32 parameter_index, float value)\n{\n    auto e = std::make_unique<ParameterChangeNotificationEvent>(this->id(),\n                                                                static_cast<ObjectId>(parameter_index),\n                                                                value,\n                                                                value,\n                                                                this->parameter_value_formatted(parameter_index).second,\n                                                                IMMEDIATE_PROCESS);\n    _host_control.post_event(std::move(e));\n}\n\nbool Vst2xWrapper::_update_speaker_arrangements(int inputs, int outputs)\n{\n    VstSpeakerArrangement in_arr;\n    VstSpeakerArrangement out_arr;\n    in_arr.numChannels = inputs;\n    in_arr.type = arrangement_from_channels(inputs);\n    out_arr.numChannels = outputs;\n    out_arr.type = arrangement_from_channels(outputs);\n    int res = _vst_dispatcher(effSetSpeakerArrangement, 0, (VstIntPtr)&in_arr, &out_arr, 0);\n    return res == 1;\n}\n\nVstTimeInfo* Vst2xWrapper::time_info()\n{\n    auto transport = _host_control.transport();\n    auto ts = transport->time_signature();\n\n    _time_info.samplePos          = static_cast<double>(transport->current_samples());\n    _time_info.sampleRate         = _sample_rate;\n    _time_info.nanoSeconds        = static_cast<double>(std::chrono::duration_cast<std::chrono::nanoseconds>(transport->current_process_time()).count());\n    _time_info.ppqPos             = transport->current_beats();\n    _time_info.tempo              = transport->current_tempo();\n    _time_info.barStartPos        = transport->current_bar_start_beats();\n    _time_info.timeSigNumerator   = ts.numerator;\n    _time_info.timeSigDenominator = ts.denominator;\n    _time_info.flags = SUSHI_HOST_TIME_CAPABILITIES | (transport->playing() ? kVstTransportPlaying : 0);\n    if (transport->current_state_change() != PlayStateChange::UNCHANGED)\n    {\n        _time_info.flags |= kVstTransportChanged;\n    }\n    return &_time_info;\n}\n\nvoid Vst2xWrapper::_map_audio_buffers(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    int i;\n\n    for (i = 0; i < _current_input_channels; ++i)\n    {\n        _process_inputs[i] = const_cast<float*>(in_buffer.channel(i));\n    }\n    for (; i <= _max_input_channels; ++i)\n    {\n        _process_inputs[i] = (_dummy_input.channel(0));\n    }\n\n    for (i = 0; i < _current_output_channels; i++)\n    {\n        _process_outputs[i] = out_buffer.channel(i);\n    }\n    for (; i <= _max_output_channels; ++i)\n    {\n        _process_outputs[i] = _dummy_output.channel(0);\n    }\n}\n\nvoid Vst2xWrapper::output_vst_event(const VstEvent* event)\n{\n    assert(event);\n    if (event->type == kVstMidiType)\n    {\n        auto midi_event = reinterpret_cast<const VstMidiEvent*>(event);\n        MidiDataByte midi_data = midi::to_midi_data_byte(reinterpret_cast<const uint8_t*>(midi_event->midiData), 3);\n        output_midi_event_as_internal(midi_data, event->deltaFrames);\n    }\n}\n\nProcessorReturnCode Vst2xWrapper::set_state(ProcessorState* state, bool realtime_running)\n{\n    if (state->program().has_value())\n    {\n        this->set_program(state->program().value());\n    }\n\n    if (realtime_running)\n    {\n        auto rt_state = std::make_unique<RtState>(*state);\n        _host_control.post_event(std::make_unique<RtStateEvent>(this->id(), std::move(rt_state), IMMEDIATE_PROCESS));\n    }\n\n    else\n    {\n        if (state->bypassed().has_value())\n        {\n            _set_bypass_rt(state->bypassed().value());\n        }\n\n        for (const auto& parameter : state->parameters())\n        {\n            VstInt32 id = parameter.first;\n            float value = parameter.second;\n            if (id < _plugin_handle->numParams)\n            {\n                _plugin_handle->setParameter(_plugin_handle, id, value);\n            }\n        }\n    }\n    return ProcessorReturnCode::OK;\n}\n\nProcessorState Vst2xWrapper::save_state() const\n{\n    ProcessorState state;\n    if (_has_binary_programs)\n    {\n        std::byte* data = nullptr;\n        int size = _vst_dispatcher(effGetChunk, SINGLE_PROGRAM, reinterpret_cast<VstIntPtr>(&data), nullptr, 0);\n        if (size > 0)\n        {\n            state.set_binary_data(std::vector<std::byte>(data, data + size));\n        }\n        if (_can_do_soft_bypass == false)\n        {\n            state.set_bypass(bypassed());\n        }\n    }\n    else\n    {\n        for (int id = 0; id < _plugin_handle->numParams; ++id)\n        {\n            float value = _plugin_handle->getParameter(_plugin_handle, static_cast<VstInt32>(id));\n            state.add_parameter_change(id, value);\n        }\n        state.set_bypass(bypassed());\n    }\n    return state;\n}\n\nPluginInfo Vst2xWrapper::info() const\n{\n    PluginInfo info;\n    info.type = PluginType::VST2X;\n    info.path = _plugin_path;\n    return info;\n}\n\nvoid Vst2xWrapper::_set_bypass_rt(bool bypassed)\n{\n    _bypass_manager.set_bypass(bypassed, _sample_rate);\n    if (_can_do_soft_bypass)\n    {\n        _vst_dispatcher(effSetBypass, 0, bypassed ? 1 : 0, nullptr, 0.0f);\n    }\n}\n\nvoid Vst2xWrapper::_set_state_rt(RtState* state)\n{\n    if (state->bypassed().has_value())\n    {\n        _set_bypass_rt(*state->bypassed());\n    }\n\n    for (const auto& parameter : state->parameters())\n    {\n        VstInt32 id = parameter.first;\n        float value = parameter.second;\n        if (id < _plugin_handle->numParams)\n        {\n            _plugin_handle->setParameter(_plugin_handle, id, value);\n        }\n    }\n    async_delete(state);\n    notify_state_change_rt();\n}\n\nVstSpeakerArrangementType arrangement_from_channels(int channels)\n{\n    switch (channels)\n    {\n        case 0:\n            return kSpeakerArrEmpty;\n        case 1:\n            return kSpeakerArrMono;\n        case 2:\n            return kSpeakerArrStereo;\n        case 3:\n            return kSpeakerArr30Music;\n        case 4:\n            return kSpeakerArr40Music;\n        case 5:\n            return kSpeakerArr50;\n        case 6:\n            return kSpeakerArr60Music;\n        case 7:\n            return kSpeakerArr70Music;\n        default:\n            return kSpeakerArr80Music;\n    }\n}\n\n} // end namespace sushi::internal::vst2\n"
  },
  {
    "path": "src/library/vst2x/vst2x_wrapper.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Wrapper for VST 2.x plugins.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_VST2X_PLUGIN_H\n#define SUSHI_VST2X_PLUGIN_H\n\n#include <map>\n\n#include \"sushi/constants.h\"\n\n#include \"library/processor.h\"\n#include \"vst2x_plugin_loader.h\"\n#include \"vst2x_midi_event_fifo.h\"\n#include \"engine/base_event_dispatcher.h\"\n\nnamespace sushi::internal::vst2 {\n\n/* Should match the maximum reasonable number of channels of a vst */\nconstexpr int VST_WRAPPER_MAX_N_CHANNELS = MAX_TRACK_CHANNELS;\nconstexpr int VST_WRAPPER_MIDI_EVENT_QUEUE_SIZE = 256;\n\n/**\n * @brief internal wrapper class for loading VST plugins and make them accessible as Processor to the Engine.\n */\n\nclass Vst2xWrapperAccessor;\n\nclass Vst2xWrapper : public Processor\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(Vst2xWrapper)\n    /**\n     * @brief Create a new Processor that wraps the plugin found in the given path.\n     */\n    Vst2xWrapper(HostControl host_control, const std::string &vst_plugin_path) :\n            Processor(host_control),\n            _sample_rate{0},\n            _process_inputs{},\n            _process_outputs{},\n            _can_do_soft_bypass{false},\n            _plugin_path{vst_plugin_path},\n            _library_handle{nullptr},\n            _plugin_handle{nullptr}\n    {\n        _max_input_channels = VST_WRAPPER_MAX_N_CHANNELS;\n        _max_output_channels = VST_WRAPPER_MAX_N_CHANNELS;\n    }\n\n    ~Vst2xWrapper() override;\n\n    /* Inherited from Processor */\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    void set_channels(int inputs, int outputs) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    bool bypassed() const override {return _bypass_manager.bypassed();}\n\n    std::pair<ProcessorReturnCode, float> parameter_value(ObjectId parameter_id) const override;\n\n    std::pair<ProcessorReturnCode, float> parameter_value_in_domain(ObjectId parameter_id) const override;\n\n    std::pair<ProcessorReturnCode, std::string> parameter_value_formatted(ObjectId parameter_id) const override;\n\n    bool supports_programs() const override {return _number_of_programs > 0;}\n\n    int program_count() const override {return _number_of_programs;}\n\n    int current_program() const override;\n\n    std::string current_program_name() const override;\n\n    std::pair<ProcessorReturnCode, std::string> program_name(int program) const override;\n\n    std::pair<ProcessorReturnCode, std::vector<std::string>> all_program_names() const override;\n\n    ProcessorReturnCode set_program(int program) override;\n\n    ProcessorReturnCode set_state(ProcessorState* state, bool realtime_running) override;\n\n    ProcessorState save_state() const override;\n\n    PluginInfo info() const override;\n\n    /**\n     * @brief Get the vst time information\n     * @return A populated VstTimeInfo struct\n     */\n    VstTimeInfo* time_info();\n\nprivate:\n    friend Vst2xWrapperAccessor;\n\n    friend VstIntPtr VSTCALLBACK host_callback(AEffect* effect,\n                                               VstInt32 opcode, VstInt32 index,\n                                               VstIntPtr value, void* ptr, float opt);\n\n    /**\n     * @brief Notify the host of a parameter change from inside the plugin\n     *        This must be called from the realtime thread\n     * @param parameter_index The index of the parameter that has changed\n     * @param value The new value of the parameter\n     */\n    void notify_parameter_change_rt(VstInt32 parameter_index, float value);\n\n    /**\n      * @brief Notify the host of a parameter change from inside the plugin\n      *        This must be called from a non-rt thread and not from the\n      *        audio thread\n      * @param parameter_index The index of the parameter that has changed\n      * @param value The new value of the parameter\n      */\n    void notify_parameter_change(VstInt32 parameter_index, float value);\n\n    /**\n     * @brief Output a vst midi event from the plugin.\n     * @param event Pointer to a VstEvent struct\n     */\n    void output_vst_event(const VstEvent* event);\n    /**\n     * @brief Tell the plugin that we're done with it and release all resources\n     * we allocated during initialization.\n     */\n    void _cleanup();\n\n    /**\n     * @brief Commodity function to access VsT internals\n     */\n    int _vst_dispatcher(VstInt32 opcode, VstInt32 index, VstIntPtr value, void* ptr, float opt) const\n    {\n        return static_cast<int>(_plugin_handle->dispatcher(_plugin_handle, opcode, index, value, ptr, opt));\n    }\n\n    /**\n     * @brief Iterate over VsT parameters and register internal FloatParameterDescriptor\n     *        for each one of them.\n     * @return True if all parameters were registered properly.\n     */\n    bool _register_parameters();\n\n    bool _update_speaker_arrangements(int inputs, int outputs);\n\n    void _map_audio_buffers(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer);\n\n    void _set_bypass_rt(bool bypassed);\n\n    void _set_state_rt(RtState* state);\n\n    float _sample_rate;\n\n    /** Wrappers for preparing data to pass to processReplacing */\n    float* _process_inputs[VST_WRAPPER_MAX_N_CHANNELS];\n    float* _process_outputs[VST_WRAPPER_MAX_N_CHANNELS];\n    ChunkSampleBuffer _dummy_input {1};\n    ChunkSampleBuffer _dummy_output {1};\n    Vst2xMidiEventFIFO<VST_WRAPPER_MIDI_EVENT_QUEUE_SIZE> _vst_midi_events_fifo;\n    bool _can_do_soft_bypass;\n    bool _has_binary_programs{false};\n    int _number_of_programs {0};\n\n    BypassManager _bypass_manager{_bypassed};\n\n    std::string _plugin_path;\n    LibraryHandle _library_handle;\n    AEffect* _plugin_handle;\n\n    VstTimeInfo _time_info;\n};\n\nVstSpeakerArrangementType arrangement_from_channels(int channels);\n\n} // end namespace sushi::internal::vst2\n\n#endif // SUSHI_VST2X_PLUGIN_H\n"
  },
  {
    "path": "src/library/vst3x/vst3x_file_utils.cpp",
    "content": "/*\n * Copyright 2017-2024 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief File and platform utility functions for VST 3.x plugins.\n * @Copyright 2017-2024 Elk Audio AB, Stockholm\n */\n\n#include <cstdlib>\n\n#include \"elklog/static_logger.h\"\n\n#ifdef _MSC_VER\n#define NOMINMAX\n#include <Windows.h>\n#include <libloaderapi.h>\n#include <shlobj_core.h>\n#undef DELETE\n#undef ERROR\n#elif defined(__APPLE__)\n#include <mach-o/dyld.h>\n#else\n#include <unistd.h>\n#endif\n\n#include \"vst3x_file_utils.h\"\n\nnamespace sushi::internal::vst3 {\n\nconstexpr char VST_PRESET_SUFFIX[] = \".vstpreset\";\nconstexpr int VST_PRESET_SUFFIX_LENGTH = 10;\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"vst3\");\n\nstd::string make_safe_folder_name(std::string name)\n{\n    // See https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/Locations+Format/Preset+Locations.html\n    constexpr std::string_view INVALID_CHARS = \"\\\\*?/:<>|\";\n    for (char i : INVALID_CHARS)\n    {\n        std::replace(name.begin(), name.end(), i, '_');\n    }\n    return name;\n}\n\nbool is_hidden(const std::filesystem::directory_entry& entry)\n{\n#ifdef _MSC_VER\n    auto attributes = GetFileAttributesA(entry.path().string().c_str());\n    return attributes & FILE_ATTRIBUTE_HIDDEN;\n#endif\n    return !entry.path().filename().empty() && entry.path().filename().c_str()[0] == '.';\n}\n\nstd::filesystem::path get_executable_path()\n{\n#ifdef _MSC_VER\n    std::array<char, 256> buffer;\n    buffer[0] = 0;\n    int res = GetModuleFileNameA(nullptr, buffer.data(), buffer.size());\n    if (res >= 0)\n    {\n        return std::filesystem::absolute(buffer.data());\n    }\n#elif defined(__APPLE__)\n    std::array<char, 256> buffer{0};\n    uint32_t  size = buffer.size();\n    int res = _NSGetExecutablePath(buffer.data(), &size);\n    if (res == 0)\n    {\n        return std::filesystem::absolute(buffer.data());\n    }\n#else\n    std::array<char, _POSIX_SYMLINK_MAX + 1> buffer{0};\n    auto path_length = readlink(\"/proc/self/exe\", buffer.data(), buffer.size() - 1);\n    if (path_length > 0)\n    {\n        return std::filesystem::absolute(buffer.data());\n    }\n#endif\n    ELKLOG_LOG_WARNING(\"Failed to get binary directory\");\n    return {};\n}\n#if defined(_MSC_VER)\nstd::vector<std::filesystem::path> get_platform_locations()\n{\n    std::vector<std::filesystem::path> locations;\n    PWSTR path = nullptr;\n    HRESULT res = SHGetKnownFolderPath(FOLDERID_Documents, 0, NULL, &path);\n    if (res == S_OK)\n    {\n        locations.emplace_back(std::filesystem::path(path) / \"VST3 Presets\");\n    }\n    res = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &path);\n    if (res == S_OK)\n    {\n        locations.emplace_back(std::filesystem::path(path) / \"VST3 Presets\");\n    }\n    res = SHGetKnownFolderPath(FOLDERID_ProgramData, 0, NULL, &path);\n    if (res == S_OK)\n    {\n        locations.emplace_back(std::filesystem::path(path) / \"VST3 Presets\");\n    }\n    auto exe_path = get_executable_path();\n    exe_path.remove_filename();\n    locations.emplace_back(exe_path / \"VST3 Presets\");\n    return locations;\n}\n\n#elif defined(__APPLE__)\nstd::vector<std::filesystem::path> get_platform_locations()\n{\n    std::vector<std::filesystem::path> locations;\n    char* home_dir = getenv(\"HOME\");\n    if (home_dir != nullptr)\n    {\n        locations.push_back(std::filesystem::path(home_dir) / \"Library\" / \"Audio\" / \"Presets\");\n    }\n    ELKLOG_LOG_WARNING_IF(home_dir == nullptr, \"Failed to get home directory\")\n    locations.emplace_back(\"/Library/Audio/Presets/\");\n    locations.emplace_back(\"/Network/Library/Audio/Presets/\");\n    auto exe_path = get_executable_path();\n    exe_path.remove_filename();\n    locations.emplace_back(std::filesystem::absolute(exe_path /\"..\"/ \"..\" / \"VST3 Presets\"));\n    return locations;\n}\n#else\nstd::vector<std::filesystem::path> get_platform_locations()\n{\n    std::vector<std::filesystem::path> locations;\n    char* home_dir = getenv(\"HOME\");\n    if (home_dir != nullptr)\n    {\n        locations.push_back(std::filesystem::path(home_dir) / \".vst3\" / \"presets\");\n    }\n    ELKLOG_LOG_WARNING_IF(home_dir == nullptr, \"Failed to get home directory\")\n    locations.emplace_back(\"/usr/share/vst3/presets/\");\n    locations.emplace_back(\"/usr/local/share/vst3/presets/\");\n    auto exe_path = get_executable_path();\n    exe_path.remove_filename();\n    locations.emplace_back(exe_path / \"vst3\" / \"presets\");\n    return locations;\n}\n#endif\n\nstd::string extract_preset_name(const std::filesystem::path& path)\n{\n    auto fname = path.filename().string();\n    return fname.substr(0, fname.length() - VST_PRESET_SUFFIX_LENGTH);\n}\n\n// Recursively search subdirs for preset files\nvoid add_patches(const std::filesystem::path& path, std::vector<std::filesystem::path>& patches)\n{\n    ELKLOG_LOG_DEBUG(\"Looking for presets in: {}\", path.string());\n    std::error_code error_code;\n    auto items = std::filesystem::directory_iterator(path, error_code);\n    if (!error_code)\n    {\n        ELKLOG_LOG_WARNING(\"Failed to open directory {} with error {} ({})\", path.string(), error_code.value(), error_code.message());\n        return;\n    }\n    for (const auto& entry : items)\n    {\n        if (entry.is_regular_file())\n        {\n            if (entry.path().string().ends_with(VST_PRESET_SUFFIX))\n            {\n                ELKLOG_LOG_DEBUG(\"Reading vst preset patch: {}\", entry.path().filename().string());\n                patches.push_back(entry.path());\n            }\n        }\n        else if (entry.is_directory() && !is_hidden(entry))\n        {\n            add_patches(entry.path(), patches);\n        }\n    }\n}\n\nstd::vector<std::filesystem::path> scan_for_presets(const std::string& plugin_name, const std::string& company)\n{\n    std::vector<std::filesystem::path> patches;\n    std::vector<std::filesystem::path> paths = get_platform_locations();\n    for (auto path : paths)\n    {\n        add_patches(path / company / plugin_name, patches);\n    }\n    return patches;\n}\n\n} // end namespace sushi::internal::vst3\n"
  },
  {
    "path": "src/library/vst3x/vst3x_file_utils.h",
    "content": "/*\n * Copyright 2017-2024 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief File access and platform helper functions for VST 3.x plugins.\n * @Copyright 2017-2024 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_LIBRARY_VST3X_FILE_UTILS_H\n#define SUSHI_LIBRARY_VST3X_FILE_UTILS_H\n\n#include <filesystem>\n\nnamespace sushi::internal::vst3 {\n\n/**\n * @brief Remove illegal characters from a string in order to use it as a folder\n *        or file name.\n * @param name the file/folder name to process\n * @return A string with illegal characters replaced with '_'\n */\nstd::string make_safe_folder_name(std::string name);\n\n/**\n * @brief Extract the preset name from a file path\n * @param The file path to extract from\n * @return The file name path minus the .vstpreset extension\n */\nstd::string extract_preset_name(const std::filesystem::path& path);\n\n/**\n * @brief Scan the platform specific locations for presets belonging to this plugin\n * @param plugin_name The name of the plugin\n * @param company The plugin vendor\n * @return A std::vector of paths to preset files\n */\nstd::vector<std::filesystem::path> scan_for_presets(const std::string& plugin_name, const std::string& company);\n\n} // end namespace sushi::internal::vst3\n\n#endif //SUSHI_LIBRARY_VST3X_FILE_UTILS_H\n"
  },
  {
    "path": "src/library/vst3x/vst3x_host_app.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief VST 3.x  plugin loader\n * @Copyright 2017-2025 Elk Audio AB, Stockholm\n */\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_EXTRA\nELK_DISABLE_DEPRECATED_DECLARATIONS\nELK_DISABLE_SHORTEN_64_TO_32\n\n#include \"pluginterfaces/base/ustring.h\"\n#include \"public.sdk/source/vst/utility/stringconvert.h\"\n#include \"base/source/fobject.h\"\n\nELK_POP_WARNING\n\n#include \"elklog/static_logger.h\"\n\n#include \"vst3x_host_app.h\"\n#include \"vst3x_wrapper.h\"\n#include \"vst3x_utils.h\"\n\n\n// Define the UUIDs for extensions:\nDEF_CLASS_IID (elk::IElkHostExtension)\nDEF_CLASS_IID (elk::IElkComponentHandlerExtension)\nDEF_CLASS_IID (elk::IElkControllerExtension)\nDEF_CLASS_IID (elk::IElkProcessorExtension)\n\nnamespace sushi::internal::vst3 {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"vst3\");\n\nconstexpr char HOST_NAME[] = \"Sushi\";\n\nSteinberg::tresult SushiHostApplication::queryInterface(const char* iid, void** obj)\n{\n    DEF_INTERFACE (elk::IElkHostExtension);\n    return HostApplication::queryInterface(iid, obj);\n}\n\nSteinberg::tresult SushiHostApplication::getName(Steinberg::Vst::String128 name)\n\n{\n    Steinberg::UString128 str(HOST_NAME);\n    str.copyTo (name, 128);\n    return Steinberg::kResultOk;\n}\n\nSteinberg::tresult SushiHostApplication::requestAsyncWork(elk::AsyncWorkCallback callback, void* data, Steinberg::int32& requestId)\n{\n    if (!callback || !twine::is_current_thread_realtime())\n    {\n        ELKLOG_LOG_WARNING_IF(!twine::is_current_thread_realtime(), \"requestAsyncWork called from outside the audio thread\");\n        return Steinberg::kInvalidArgument;\n    }\n\n    auto id = _wrapper_instance->request_async_work(callback, data);\n    requestId = id;\n    return Steinberg::kResultOk;\n}\n\nComponentHandler::ComponentHandler(Vst3xWrapper* wrapper_instance,\n                                   HostControl* host_control) : _wrapper_instance(wrapper_instance),\n                                                                _host_control(host_control)\n{}\n\nSteinberg::tresult ComponentHandler::queryInterface(const char* iid, void** obj)\n{\n    DEF_INTERFACE (elk::IElkComponentHandlerExtension);\n    DEF_INTERFACE (Steinberg::Vst::IComponentHandler);\n    return Steinberg::kResultFalse;\n}\n\nSteinberg::tresult ComponentHandler::performEdit(Steinberg::Vst::ParamID parameter_id,\n                                                 Steinberg::Vst::ParamValue normalized_value)\n{\n    ELKLOG_LOG_DEBUG(\"performEdit called, param: {} value: {}\", parameter_id, normalized_value);\n    _wrapper_instance->set_parameter_change(ObjectId(parameter_id), static_cast<float>(normalized_value));\n    return Steinberg::kResultOk;\n}\n\nSteinberg::tresult ComponentHandler::restartComponent(Steinberg::int32 flags)\n{\n    ELKLOG_LOG_DEBUG(\"restartComponent called\");\n    if (flags | (Steinberg::Vst::kParamValuesChanged & Steinberg::Vst::kReloadComponent))\n    {\n        auto event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_UPDATED,\n                                                                   _wrapper_instance->id(), 0, IMMEDIATE_PROCESS);\n        _host_control->post_event(std::move(event));\n        return Steinberg::kResultOk;\n    }\n    return Steinberg::kResultFalse;\n}\n\nSteinberg::tresult ComponentHandler::notifyPropertyValueChange(Steinberg::int32 propertyId, const elk::PropertyValue& value)\n{\n    if (twine::is_current_thread_realtime())\n    {\n        ELKLOG_LOG_WARNING(\"notifyPropertyValueChange() called from a realtime thread\");\n        return Steinberg::kResultFalse;\n    }\n    _host_control->post_event(std::make_unique<PropertyChangeNotificationEvent>(_wrapper_instance->id(),\n                                                                                propertyId,\n                                                                                std::string(value.value, value.length),\n                                                                                IMMEDIATE_PROCESS));\n    return Steinberg::kResultOk;\n}\n\n/* ConnectionProxy is more or less ripped straight out of Steinberg example code.\n * But edited to follow Elk coding style. See plugprovider.cpp. */\nclass ConnectionProxy : public Steinberg::FObject, public Steinberg::Vst::IConnectionPoint\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(ConnectionProxy);\n\n    explicit ConnectionProxy(Steinberg::Vst::IConnectionPoint* src_connection) : _source_connection(src_connection) {}\n    ~ConnectionProxy() override = default;\n\n    Steinberg::tresult connect(Steinberg::Vst::IConnectionPoint* other) override;\n    Steinberg::tresult disconnect(Steinberg::Vst::IConnectionPoint* other) override;\n    Steinberg::tresult notify(Steinberg::Vst::IMessage* message) override;\n\n    bool disconnect();\n\n    OBJ_METHODS(ConnectionProxy, FObject)\n    REFCOUNT_METHODS(FObject)\n    DEF_INTERFACES_1(IConnectionPoint, FObject)\n\nprotected:\n    Steinberg::IPtr<IConnectionPoint> _source_connection;\n    Steinberg::IPtr<IConnectionPoint> _dest_connection;\n};\n\nSteinberg::tresult PLUGIN_API ConnectionProxy::connect(IConnectionPoint* other)\n{\n    if (other == nullptr)\n    {\n        return Steinberg::kInvalidArgument;\n    }\n\n    if (_dest_connection)\n    {\n        return Steinberg::kResultFalse;\n    }\n\n    _dest_connection = other;\n    Steinberg::tresult res = _source_connection->connect(this);\n    if (res != Steinberg::kResultTrue)\n    {\n        _dest_connection = nullptr;\n    }\n    return res;\n}\n\nSteinberg::tresult PLUGIN_API ConnectionProxy::disconnect(IConnectionPoint* other)\n{\n    if (other == nullptr)\n    {\n        return Steinberg::kInvalidArgument;\n    }\n\n    if (other == _dest_connection)\n    {\n        if (_source_connection)\n        {\n            _source_connection->disconnect(this);\n        }\n        _dest_connection = nullptr;\n        return Steinberg::kResultTrue;\n    }\n    return Steinberg::kInvalidArgument;\n}\n\nSteinberg::tresult PLUGIN_API ConnectionProxy::notify(Steinberg::Vst::IMessage* message)\n{\n    if (_dest_connection)\n    {\n        return _dest_connection->notify(message);\n    }\n    return Steinberg::kResultFalse;\n}\n\nbool ConnectionProxy::disconnect()\n{\n    auto status =  disconnect(_dest_connection);\n    return  status == Steinberg::kResultTrue;\n}\n\nPluginInstance::PluginInstance(SushiHostApplication* host_app): _host_app(host_app)\n{}\n\nPluginInstance::~PluginInstance()\n{\n    if (_component_connection)\n    {\n        _component_connection->disconnect();\n    }\n    if (_controller_connection)\n    {\n        _controller_connection->disconnect();\n    }\n}\n\nbool PluginInstance::load_plugin(const std::string& plugin_path, const std::string& plugin_name, Steinberg::Vst::IComponentHandler* component_handler)\n{\n    std::string error_msg;\n    _module = VST3::Hosting::Module::create(plugin_path, error_msg);\n    if (!_module)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to load VST3 Module: {}\", error_msg);\n        return false;\n    }\n    auto factory = _module->getFactory().get();\n    if (!factory)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to get PluginFactory, plugin is probably broken\");\n        return false;\n    }\n    Steinberg::PFactoryInfo info;\n    if (factory->getFactoryInfo(&info) != Steinberg::kResultOk)\n    {\n        return false;\n    }\n    // In the future we might want to check for more things than just vendor name here\n    _vendor = info.vendor;\n\n    auto component = load_component(factory, plugin_name);\n    if (!component)\n    {\n        return false;\n    }\n\n    auto res = component->initialize(static_cast<Steinberg::Vst::IHostApplication*>(_host_app));\n    if (res != Steinberg::kResultOk)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to initialize component with error code: {}\", res);\n        return false;\n    }\n\n    auto processor = query_vst_interface<Steinberg::Vst::IAudioProcessor>(component);\n    if (!processor)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to get processor from component\");\n        return false;\n    }\n\n    auto controller = load_controller(factory, component);\n    if (!controller)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to load controller\");\n        return false;\n    }\n\n    res = controller->initialize(static_cast<Steinberg::Vst::IHostApplication*>(_host_app));\n    if (res != Steinberg::kResultOk)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to initialize component with error code: {}\", res);\n        return false;\n    }\n\n    res = controller->setComponentHandler(component_handler);\n    if (res != Steinberg::kResultOk)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to set component handler with error code: {}\", res);\n        return false;\n    }\n\n    _component = component;\n    _processor = processor;\n    _controller = controller;\n    _name = plugin_name;\n\n    if (!_connect_components())\n    {\n        ELKLOG_LOG_ERROR(\"Failed to connect component to editor\");\n    }\n    _query_extension_interfaces();\n\n    return true;\n}\n\nbool PluginInstance::load_plugin_from_component(Steinberg::Vst::IComponent* component,\n                                                const std::string& plugin_name,\n                                                Steinberg::Vst::IComponentHandler* component_handler)\n{\n    assert(component);\n    auto res = component->initialize(static_cast<Steinberg::Vst::IHostApplication*>(_host_app));\n    if (res != Steinberg::kResultOk)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to initialize component with error code: {}\", res);\n        return false;\n    }\n    auto processor = query_vst_interface<Steinberg::Vst::IAudioProcessor>(component);\n    if (!processor)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to get processor from component\");\n        return false;\n    }\n    auto controller = query_vst_interface<Steinberg::Vst::IEditController>(component);\n    if (!controller)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to get controller from component\");\n        return false;\n    }\n    res = controller->setComponentHandler(component_handler);\n    if (res != Steinberg::kResultOk)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to set component handler with error code: {}\", res);\n        return false;\n    }\n\n    _component = component;\n    _processor = processor;\n    _controller = controller;\n    _name = plugin_name;\n\n    _query_extension_interfaces();\n\n    if (_connect_components() == false)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to connect component to editor\");\n    }\n    return true;\n}\n\nvoid PluginInstance::_query_extension_interfaces()\n{\n    auto midi_mapper = query_vst_interface<Steinberg::Vst::IMidiMapping>(_controller.get());\n    if (midi_mapper)\n    {\n        _midi_mapper = midi_mapper;\n        ELKLOG_LOG_INFO(\"Plugin supports Midi Mapping interface\");\n    }\n\n    auto unit_info = query_vst_interface<Steinberg::Vst::IUnitInfo>(_controller.get());\n    if (unit_info)\n    {\n        _unit_info = unit_info;\n        ELKLOG_LOG_INFO(\"Plugin supports Unit Info interface for programs\");\n    }\n\n    auto processor_extension = query_vst_interface<::elk::IElkProcessorExtension>(_processor.get());\n    if (processor_extension)\n    {\n        _elk_processor_extension = processor_extension;\n        ELKLOG_LOG_INFO(\"Plugin supports Elk Processor Extension\");\n    }\n\n    auto controller_extension = query_vst_interface<elk::IElkControllerExtension>(_controller.get());\n    if (controller_extension)\n    {\n        _elk_controller_extension = controller_extension;\n        ELKLOG_LOG_INFO(\"Plugin supports Elk Controller Extension\");\n    }\n}\n\nbool PluginInstance::_connect_components()\n{\n    Steinberg::FUnknownPtr<Steinberg::Vst::IConnectionPoint> component_connection(_component);\n    Steinberg::FUnknownPtr<Steinberg::Vst::IConnectionPoint> controller_connection(_controller);\n\n    if (!component_connection || !controller_connection)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to create connection points\");\n        return false;\n    }\n\n    _component_connection = NEW ConnectionProxy(component_connection);\n    _controller_connection = NEW ConnectionProxy(controller_connection);\n\n    if (_component_connection->connect(controller_connection) != Steinberg::kResultTrue)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to connect component\");\n    }\n    else\n    {\n        if (_controller_connection->connect(component_connection) != Steinberg::kResultTrue)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to connect controller\");\n        }\n        else\n        {\n            return true;\n        }\n    }\n    return false;\n}\n\nbool PluginInstance::notify_controller(Steinberg::Vst::IMessage*message)\n{\n    // This calls notify() on the component connection proxy, which is has the controller\n    // connected as its destination. So it is the controller being notified\n    auto res = _component_connection->notify(message);\n    if (res == Steinberg::kResultOk || res == Steinberg::kResultFalse)\n    {\n        return true;\n    }\n    return false;\n}\n\nbool PluginInstance::notify_processor(Steinberg::Vst::IMessage*message)\n{\n    auto res = _controller_connection->notify(message);\n    if (res == Steinberg::kResultOk || res == Steinberg::kResultFalse)\n    {\n        return true;\n    }\n    return false;\n}\n\nSteinberg::Vst::IComponent* load_component(Steinberg::IPluginFactory* factory,\n                                           const std::string& plugin_name)\n{\n    for (int i = 0; i < factory->countClasses(); ++i)\n    {\n        Steinberg::PClassInfo info;\n        factory->getClassInfo(i, &info);\n        ELKLOG_LOG_INFO(\"Querying plugin {} of type {}\", info.name, info.category);\n        if (info.name == plugin_name)\n        {\n            Steinberg::Vst::IComponent* component;\n            auto res = factory->createInstance(info.cid, Steinberg::Vst::IComponent::iid,\n                                               reinterpret_cast<void**>(&component));\n            if (res == Steinberg::kResultOk)\n            {\n                ELKLOG_LOG_INFO(\"Creating plugin {}\", info.name);\n                return component;\n            }\n            ELKLOG_LOG_ERROR(\"Failed to create component with error code: {}\", res);\n            return nullptr;\n        }\n    }\n    ELKLOG_LOG_ERROR(\"No match for plugin {} in factory\", plugin_name);\n    return nullptr;\n}\n\nSteinberg::Vst::IEditController* load_controller(Steinberg::IPluginFactory* factory,\n                                                  Steinberg::Vst::IComponent* component)\n{\n    /* The controller can be implemented both as a part of the component (if inhering from\n     * SingleComponentEffect) or as a separate object, Steinberg recommends the latter,\n     * JUCE does the former in their plugin adaptor. */\n\n    auto controller = query_vst_interface<Steinberg::Vst::IEditController>(component);\n    if (controller)\n    {\n        return controller;\n    }\n    // Else try to instantiate the controller as a separate object.\n    Steinberg::TUID controllerTUID;\n    if (component->getControllerClassId(controllerTUID) == Steinberg::kResultTrue)\n    {\n        Steinberg::FUID controllerID(controllerTUID);\n        if (controllerID.isValid())\n        {\n            auto res = factory->createInstance(controllerID, Steinberg::Vst::IEditController::iid,\n                                               reinterpret_cast<void**>(&controller));\n            if (res == Steinberg::kResultOk)\n            {\n                return controller;\n            }\n            ELKLOG_LOG_ERROR(\"Failed to create controller with error code: {}\", res);\n        }\n    }\n    return nullptr;\n}\n\n} // end namespace sushi::internal::vst3\n"
  },
  {
    "path": "src/library/vst3x/vst3x_host_app.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief VST 3.x  plugin loader\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_VST3X_HOST_CONTEXT_H\n#define SUSHI_VST3X_HOST_CONTEXT_H\n\n#include \"sushi/constants.h\"\n\n#include \"library/id_generator.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_EXTRA\nELK_DISABLE_DEPRECATED_DECLARATIONS\nELK_DISABLE_SHORTEN_64_TO_32\n\n#include \"pluginterfaces/vst/ivsteditcontroller.h\"\n#include \"pluginterfaces/vst/ivstunits.h\"\n#include \"pluginterfaces/vst/ivsthostapplication.h\"\n#include \"pluginterfaces/vst/ivstaudioprocessor.h\"\n#include \"pluginterfaces/vst/ivstcomponent.h\"\n#include \"public.sdk/source/vst/hosting/module.h\"\n#include \"base/source/fobject.h\"\n#include \"public.sdk/source/vst/hosting/hostclasses.h\"\n#include \"elk_vst3_extensions/elk_vst3_extensions.h\"\n\nELK_POP_WARNING\n\nnamespace sushi::internal {\n\nclass HostControl;\n\nnamespace vst3 {\n\nclass Vst3xWrapper;\nclass ConnectionProxy;\n\nclass SushiHostApplication : public Steinberg::Vst::HostApplication, public elk::IElkHostExtension\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(SushiHostApplication);\n\n    SushiHostApplication(Vst3xWrapper* wrapper_instance) : Steinberg::Vst::HostApplication(),\n                                                           _wrapper_instance(wrapper_instance) {}\n\n    Steinberg::tresult PLUGIN_API queryInterface (const Steinberg::TUID iid, void** obj) override;\n\n    Steinberg::tresult PLUGIN_API getName (Steinberg::Vst::String128 name) override;\n\n    Steinberg::tresult PLUGIN_API requestAsyncWork(elk::AsyncWorkCallback callback, void* data, Steinberg::int32& requestId) override;\n\n    // Refer addRef/release functions to the base class implementation\n    REFCOUNT_METHODS(Steinberg::Vst::HostApplication);\n\nprivate:\n    Vst3xWrapper* _wrapper_instance;\n};\n\nclass ComponentHandler : public Steinberg::Vst::IComponentHandler,  public elk::IElkComponentHandlerExtension, public Steinberg::FObject\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(ComponentHandler);\n\n    explicit ComponentHandler(Vst3xWrapper* wrapper_instance, HostControl* host_control);\n\n    Steinberg::tresult PLUGIN_API queryInterface (const Steinberg::TUID iid, void** obj) override;\n\n    Steinberg::tresult PLUGIN_API beginEdit (Steinberg::Vst::ParamID /*id*/) override {return Steinberg::kNotImplemented;}\n    Steinberg::tresult PLUGIN_API performEdit (Steinberg::Vst::ParamID parameter_id, Steinberg::Vst::ParamValue normalized_value) override;\n    Steinberg::tresult PLUGIN_API endEdit (Steinberg::Vst::ParamID /*parameter_id*/) override {return Steinberg::kNotImplemented;}\n    Steinberg::tresult PLUGIN_API restartComponent (Steinberg::int32 flags) override;\n\n    // From elk::IComponentHandlerExtension\n    Steinberg::tresult PLUGIN_API notifyPropertyValueChange(Steinberg::int32 propertyId, const elk::PropertyValue& value) override;\n\n    // Refer addRef/release functions to the base class implementation\n    REFCOUNT_METHODS(Steinberg::FObject);\n\nprivate:\n    Vst3xWrapper* _wrapper_instance;\n    HostControl*  _host_control;\n};\n\n/**\n * @brief Container to hold plugin modules and manage their lifetimes\n */\nclass PluginInstance\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(PluginInstance);\n\n    explicit PluginInstance(SushiHostApplication* host_app);\n    ~PluginInstance();\n\n    /**\n     * @brief Load a plugin from file\n     * @param plugin_path Path to the .vst file or folder\n     * @param plugin_name Name of the plugin to load from the file, vst3 allows you to pack multiple plugins in one file/bundle\n     * @return True if successful, false otherwise\n     */\n    bool load_plugin(const std::string& plugin_path, const std::string& plugin_name, Steinberg::Vst::IComponentHandler* component_handler);\n\n    /**\n     * @brief Load a plugin from an already instantiated component. This assumes the plugin is built from SingleComponentEffect\n     * @param component The component to load from\n     * @param plugin_name Name of the plugin to load\n     * @return True if successful, false otherwise\n     */\n    bool load_plugin_from_component(Steinberg::Vst::IComponent* component, const std::string& plugin_name, Steinberg::Vst::IComponentHandler* component_handler);\n\n    [[nodiscard]] const std::string& name() const {return _name;}\n    [[nodiscard]] const std::string& vendor() const {return _vendor;}\n    [[nodiscard]] Steinberg::Vst::IComponent* component() const {return _component.get();}\n    [[nodiscard]] Steinberg::Vst::IAudioProcessor* processor() const {return _processor.get();}\n    [[nodiscard]] Steinberg::Vst::IEditController* controller() const {return _controller.get();}\n    [[nodiscard]] Steinberg::Vst::IUnitInfo* unit_info() {return _unit_info;}\n    [[nodiscard]] Steinberg::Vst::IMidiMapping* midi_mapper() {return _midi_mapper;}\n    [[nodiscard]] elk::IElkControllerExtension* controller_extension() {return _elk_controller_extension.get();}\n    [[nodiscard]] elk::IElkProcessorExtension* processor_extension() {return _elk_processor_extension.get();}\n\n    bool notify_controller(Steinberg::Vst::IMessage* message);\n    bool notify_processor(Steinberg::Vst::IMessage* message);\n\nprivate:\n    void _query_extension_interfaces();\n    bool _connect_components();\n\n    std::string _name;\n    std::string _vendor;\n\n    SushiHostApplication* _host_app;\n\n    std::shared_ptr<VST3::Hosting::Module> _module;\n\n    // Reference counted pointers to plugin objects\n    Steinberg::OPtr<Steinberg::Vst::IComponent>      _component{nullptr};\n    Steinberg::OPtr<Steinberg::Vst::IAudioProcessor> _processor{nullptr};\n    Steinberg::OPtr<Steinberg::Vst::IEditController> _controller{nullptr};\n\n    // Abstract optional interfaces implemented by one of the objects above:\n    Steinberg::OPtr<Steinberg::Vst::IMidiMapping>    _midi_mapper{nullptr};\n    Steinberg::OPtr<Steinberg::Vst::IUnitInfo>       _unit_info{nullptr};\n\n    // Optional Elk specific extension interfaces implemented by the plugin\n\n    Steinberg::OPtr<elk::IElkProcessorExtension>     _elk_processor_extension{nullptr};\n    Steinberg::OPtr<elk::IElkControllerExtension>    _elk_controller_extension{nullptr};\n\n    Steinberg::OPtr<ConnectionProxy> _controller_connection;\n    Steinberg::OPtr<ConnectionProxy> _component_connection;\n};\n\nSteinberg::Vst::IComponent* load_component(Steinberg::IPluginFactory* factory, const std::string& plugin_name);\nSteinberg::Vst::IEditController* load_controller(Steinberg::IPluginFactory* factory, Steinberg::Vst::IComponent*);\n\n} // end namespace vst\n} // end namespace sushi::internal\n\n#endif // SUSHI_VST3X_HOST_CONTEXT_H\n"
  },
  {
    "path": "src/library/vst3x/vst3x_processor_factory.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Factory implementation for VST3 processors.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"vst3x_processor_factory.h\"\n\n#ifdef SUSHI_BUILD_WITH_VST3\n#include \"vst3x_host_app.h\"\n#include \"library/vst3x/vst3x_wrapper.h\"\n#endif\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_UNUSED_CONST_VARIABLE\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"Vst3\");\nELK_POP_WARNING\n\nnamespace sushi::internal::vst3 {\n\nVst3xProcessorFactory::~Vst3xProcessorFactory() = default;\n\n#ifdef SUSHI_BUILD_WITH_VST3\n\nVst3xProcessorFactory::Vst3xProcessorFactory() = default;\n\nstd::pair<ProcessorReturnCode, std::shared_ptr<Processor>> Vst3xProcessorFactory::new_instance(const PluginInfo& plugin_info,\n                                                                                               HostControl& host_control,\n                                                                                               float sample_rate)\n{\n    auto processor = std::make_shared<Vst3xWrapper>(host_control,\n                                                    plugin_info.path,\n                                                    plugin_info.uid);\n    auto processor_status = processor->init(sample_rate);\n    return {processor_status, processor};\n}\n\n#else // SUSHI_BUILD_WITH_VST3\n\nclass SushiHostApplication {};\n\nVst3xProcessorFactory::Vst3xProcessorFactory() = default;\n\nstd::pair<ProcessorReturnCode, std::shared_ptr<Processor>> Vst3xProcessorFactory::new_instance([[maybe_unused]] const PluginInfo& plugin_info,\n                                                                                               [[maybe_unused]] HostControl& host_control,\n                                                                                               [[maybe_unused]] float sample_rate)\n{\n    ELKLOG_LOG_ERROR(\"Sushi was not built with support for VST3 plugins\");\n    return {ProcessorReturnCode::UNSUPPORTED_OPERATION, nullptr};\n}\n\n#endif // SUSHI_BUILD_WITH_VST3\n\n} // end namespace sushi::internal::vst3\n"
  },
  {
    "path": "src/library/vst3x/vst3x_processor_factory.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Factory for VST3 processors.\n */\n\n#ifndef SUSHI_VST3X_PROCESSOR_FACTORY_H\n#define SUSHI_VST3X_PROCESSOR_FACTORY_H\n\n#include <memory>\n\n#include \"library/base_processor_factory.h\"\n\nnamespace sushi::internal::vst3 {\n\nclass SushiHostApplication;\n\nclass Vst3xProcessorFactory : public BaseProcessorFactory\n{\npublic:\n    Vst3xProcessorFactory();\n    ~Vst3xProcessorFactory() override;\n\n    std::pair<ProcessorReturnCode, std::shared_ptr<Processor>> new_instance(const PluginInfo& plugin_info,\n                                                                            HostControl& host_control,\n                                                                            float sample_rate) override;\n};\n\n} // end namespace sushi::internal::vst3\n\n#endif // SUSHI_VST3X_PROCESSOR_FACTORY_H\n"
  },
  {
    "path": "src/library/vst3x/vst3x_utils.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Helper classes for VST 3.x plugins.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"vst3x_utils.h\"\n\nnamespace sushi::internal::vst3 {\n\nvoid SushiProcessData::assign_buffers(const ChunkSampleBuffer& input, ChunkSampleBuffer& output,\n                                      int in_channels, int out_channels)\n{\n    assert(input.channel_count() <= VST_WRAPPER_MAX_N_CHANNELS &&\n           output.channel_count() <= VST_WRAPPER_MAX_N_CHANNELS);\n    for (int i = 0; i < input.channel_count(); ++i)\n    {\n        _process_inputs[i] = const_cast<float*>(input.channel(i));\n    }\n    for (int i = 0; i < output.channel_count(); ++i)\n    {\n        _process_outputs[i] = output.channel(i);\n    }\n    inputs->numChannels = in_channels;\n    outputs->numChannels = out_channels;\n}\n\nSteinberg::Vst::Event convert_note_on_event(const KeyboardRtEvent* event)\n{\n    assert(event->type() == RtEventType::NOTE_ON);\n    Steinberg::Vst::Event vst_event;\n    vst_event.busIndex = 0;\n    vst_event.sampleOffset = event->sample_offset();\n    vst_event.ppqPosition = 0;\n    vst_event.flags = 0;\n    vst_event.type = Steinberg::Vst::Event::kNoteOnEvent;\n    vst_event.noteOn.channel = static_cast<Steinberg::int16>(event->channel());\n    vst_event.noteOn.pitch = static_cast<Steinberg::int16>(event->note());\n    vst_event.noteOn.tuning = 0.0f;\n    vst_event.noteOn.velocity = event->velocity();\n    vst_event.noteOn.length = 0;\n    vst_event.noteOn.noteId = -1;\n    return vst_event;\n}\n\nSteinberg::Vst::Event convert_note_off_event(const KeyboardRtEvent* event)\n{\n    assert(event->type() == RtEventType::NOTE_OFF);\n    Steinberg::Vst::Event vst_event;\n    vst_event.busIndex = 0;\n    vst_event.sampleOffset = event->sample_offset();\n    vst_event.ppqPosition = 0;\n    vst_event.flags = 0;\n    vst_event.type = Steinberg::Vst::Event::kNoteOffEvent;\n    vst_event.noteOff.channel = static_cast<Steinberg::int16>(event->channel());\n    vst_event.noteOff.pitch = static_cast<Steinberg::int16>(event->note());\n    vst_event.noteOff.velocity = event->velocity();\n    vst_event.noteOff.noteId = -1;\n    vst_event.noteOff.tuning = 0.0f;\n    return vst_event;\n}\n\nSteinberg::Vst::Event convert_aftertouch_event(const KeyboardRtEvent* event)\n{\n    assert(event->type() == RtEventType::NOTE_AFTERTOUCH);\n    Steinberg::Vst::Event vst_event;\n    vst_event.busIndex = 0;\n    vst_event.sampleOffset = event->sample_offset();\n    vst_event.ppqPosition = 0;\n    vst_event.flags = 0;\n    vst_event.type = Steinberg::Vst::Event::kPolyPressureEvent;\n    vst_event.polyPressure.channel = static_cast<Steinberg::int16>(event->channel());\n    vst_event.polyPressure.pitch = static_cast<Steinberg::int16>(event->note());\n    vst_event.polyPressure.pressure = event->velocity();\n    vst_event.polyPressure.noteId = -1;\n    return vst_event;\n}\n\nvoid StateParamValue::set_values(const ObjectId& id, const float& value)\n{\n    _id = id;\n    _value = value;\n}\n\nSteinberg::tresult StateParamValue::getPoint(Steinberg::int32 /*id*/,\n                                             Steinberg::int32& sampleOffset,\n                                             Steinberg::Vst::ParamValue& value)\n{\n    sampleOffset = 0;\n    value = _value;\n    return Steinberg::kResultOk;\n}\n\nSteinberg::int32 Vst3xRtState::getParameterCount()\n{\n    return static_cast<Steinberg::int32>(_parameter_changes.size());\n}\n\n\nSteinberg::Vst::IParamValueQueue* Vst3xRtState::getParameterData(Steinberg::int32 index)\n{\n    if (static_cast<size_t>(index) >= _parameter_changes.size())\n    {\n        return nullptr;\n    }\n    const auto& value = _parameter_changes[index];\n    _transfer_value.set_values(value.first, value.second);\n    return &_transfer_value;\n}\n\nSteinberg::Vst::IParamValueQueue* Vst3xRtState::addParameterData(const Steinberg::Vst::ParamID& /*id*/,\n                                                                 Steinberg::int32& /*index*/)\n{\n    return nullptr;\n}\n\n} // end namespace sushi::internal::vst3\n"
  },
  {
    "path": "src/library/vst3x/vst3x_utils.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Helper classes for VST 3.x plugins.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_VST3X_UTILS_H\n#define SUSHI_VST3X_UTILS_H\n\n#include <cassert>\n\n#include \"sushi/constants.h\"\n#include \"sushi/sample_buffer.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_EXTRA\nELK_DISABLE_DEPRECATED_DECLARATIONS\nELK_DISABLE_SHORTEN_64_TO_32\n\n#include \"pluginterfaces/base/ipluginbase.h\"\n#include \"pluginterfaces/vst/ivstaudioprocessor.h\"\n#include \"pluginterfaces/vst/ivstparameterchanges.h\"\n#include \"pluginterfaces/vst/ivstevents.h\"\n#include \"public.sdk/source/vst/hosting/eventlist.h\"\n#include \"public.sdk/source/vst/hosting/parameterchanges.h\"\n\nELK_POP_WARNING\n\n#include \"library/rt_event.h\"\n#include \"library/processor_state.h\"\n\nnamespace sushi::internal::vst3 {\n\nconstexpr int VST_WRAPPER_MAX_N_CHANNELS = MAX_TRACK_CHANNELS;\n\n/**\n * @brief Query and return an interface class if the passed component implements it\n */\ntemplate <typename InterfaceClass>\nInterfaceClass* query_vst_interface(Steinberg::FUnknown* component)\n{\n    InterfaceClass* interface;\n    auto res = component->queryInterface (InterfaceClass::iid, reinterpret_cast<void**>(&interface));\n    if (res == Steinberg::kResultOk)\n    {\n        return interface;\n    }\n    return nullptr;\n}\n\n/**\n * @brief Wrapping the processdata in our own class for convenience\n */\nclass SushiProcessData : public Steinberg::Vst::ProcessData\n{\npublic:\n    SushiProcessData(Steinberg::Vst::EventList* in_event_list,\n                     Steinberg::Vst::EventList* out_event_list,\n                     Steinberg::Vst::ParameterChanges* in_parameter_changes,\n                     Steinberg::Vst::ParameterChanges* out_parameter_changes) :\n                                    _in_events(in_event_list),\n                                    _out_events(out_event_list),\n                                    _in_parameters(in_parameter_changes),\n                                    _out_parameters(out_parameter_changes)\n    {\n        _input_buffers.channelBuffers32 = _process_inputs;\n        _output_buffers.channelBuffers32 = _process_outputs;\n        inputs = &_input_buffers;\n        outputs = &_output_buffers;\n        numInputs = 1;  /* Note: number of buses, not channels */\n        numOutputs = 1; /* Note: number of buses, not channels */\n        numSamples = AUDIO_CHUNK_SIZE;\n        symbolicSampleSize = Steinberg::Vst::SymbolicSampleSizes::kSample32;\n        processMode = Steinberg::Vst::ProcessModes::kRealtime;\n        processContext = &_context;\n\n        inputEvents = in_event_list;\n        outputEvents = out_event_list;\n        inputParameterChanges = in_parameter_changes;\n        outputParameterChanges = out_parameter_changes;\n    }\n    /**\n     * @brief Re-map the internal buffers to point to the given samplebuffers.\n     *        Use before calling process(data)\n     * @param input Input buffers\n     * @param output Output buffers\n     */\n    void assign_buffers(const ChunkSampleBuffer& input, ChunkSampleBuffer& output, int in_channels, int out_channels);\n\n    /**\n     * @brief Clear all event and parameter changes to prepare for a new round\n     *        of processing. Call when process(data) has returned\n     */\n    void clear()\n    {\n        _in_events->clear();\n        _out_events->clear();\n        _in_parameters->clearQueue();\n        _out_parameters->clearQueue();\n    }\n\nprivate:\n    float* _process_inputs[VST_WRAPPER_MAX_N_CHANNELS];\n    float* _process_outputs[VST_WRAPPER_MAX_N_CHANNELS];\n    Steinberg::Vst::AudioBusBuffers _input_buffers;\n    Steinberg::Vst::AudioBusBuffers _output_buffers;\n    Steinberg::Vst::ProcessContext  _context;\n    /* Keep pointers to the implementations, so we can call clear on them. */\n    Steinberg::Vst::EventList* _in_events;\n    Steinberg::Vst::EventList* _out_events;\n    Steinberg::Vst::ParameterChanges* _in_parameters;\n    Steinberg::Vst::ParameterChanges* _out_parameters;\n};\n\n/**\n * @brief Convert a Sushi NoteOn event to a Vst3 Note on event\n * @param event A Sushi note on event\n * @return a Vst3 note on event\n */\nSteinberg::Vst::Event convert_note_on_event(const KeyboardRtEvent* event);\n\n/**\n * @brief Convert a Sushi NoteOff event to a Vst3 Note off event\n * @param event A Sushi note off event\n * @return a Vst3 note off event\n */\nSteinberg::Vst::Event convert_note_off_event(const KeyboardRtEvent* event);\n\n/**\n * @brief Convert a Sushi Aftertouch event to a Vst3 event\n * @param event A Sushi Aftertouch event\n * @return a Vst3 poly pressure event\n */\nSteinberg::Vst::Event convert_aftertouch_event(const KeyboardRtEvent* event);\n\n/**\n * @brief Custom implementation of IParamValueQueue to work with Vst3xRtState below\n */\nclass StateParamValue : public Steinberg::Vst::IParamValueQueue\n{\npublic:\n    StateParamValue(ObjectId id, const float value) : _id(id), _value(value) {}\n\n    void set_values(const ObjectId& id, const float& value);\n\n    // Inherited from IParamValueQueue\n    Steinberg::Vst::ParamID getParameterId() override {return _id;};\n\n    Steinberg::int32 getPointCount() override {return 1;}\n\n    Steinberg::tresult getPoint(Steinberg::int32 /*index*/, Steinberg::int32& sampleOffset /*out*/,\n                                Steinberg::Vst::ParamValue& value /*out*/) override;\n\n    // The functions below should not be called, as this container should not be added too\n    Steinberg::tresult addPoint(Steinberg::int32 /*sampleOffset*/, Steinberg::Vst::ParamValue /*value*/,\n                                Steinberg::int32& /*index*/) override {return Steinberg::kResultFalse;};\n\n    // These are added for completeness, but this class should not be used with Steinbergs reference counters pointers\n    Steinberg::tresult queryInterface(const Steinberg::TUID /*_iid*/, void** /*obj*/) override\n    {\n        return Steinberg::kNotImplemented;\n    }\n\n    Steinberg::uint32 addRef() override {return 1;}\n\n    Steinberg::uint32 release() override {return 1;}\n\nprivate:\n    ObjectId _id;\n    float _value;\n};\n\n/**\n * @brief The implementation of Steinberg::Vst::ParameterChanges supplied with the vst sdk is\n * much too inefficient for setting a large number of parameters during one audio process call.\n * Instead we wrap RtState, which has parameter changes stored sequentially in a contiguous\n * block of memory in an interface that plugins can access directly */\nclass Vst3xRtState : public RtState, public Steinberg::Vst::IParameterChanges\n{\npublic:\n    explicit Vst3xRtState(const ProcessorState& state) : RtState(state) {}\n\n    Steinberg::int32 getParameterCount () override;\n\n    Steinberg::Vst::IParamValueQueue* getParameterData (Steinberg::int32 index) override;\n\n    Steinberg::Vst::IParamValueQueue* addParameterData (const Steinberg::Vst::ParamID& /*id*/, Steinberg::int32& /*index*/) override;\n\n    // These are added for completeness, but this class should not be used with Steinbergs reference counters pointers\n    Steinberg::tresult queryInterface (const Steinberg::TUID /*_iid*/, void** /*obj*/) override\n    {\n        return Steinberg::kNotImplemented;\n    };\n\n    Steinberg::uint32 addRef() override {return 1;};\n\n    Steinberg::uint32 release() override {return 1;};\n\nprivate:\n    StateParamValue _transfer_value{0, 0.0f};\n};\n\n} // end namespace sushi::internal::vst3\n\n#endif // SUSHI_VST3X_UTILS_H\n"
  },
  {
    "path": "src/library/vst3x/vst3x_wrapper.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Wrapper for VST 3.x plugins.\n * @Copyright 2017-2024 Elk Audio AB, Stockholm\n */\n\n#include <string>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_EXTRA\nELK_DISABLE_DEPRECATED_DECLARATIONS\nELK_DISABLE_SHORTEN_64_TO_32\n\n#include \"pluginterfaces/base/ustring.h\"\n#include \"pluginterfaces/vst/ivstmidicontrollers.h\"\n#include \"public.sdk/source/vst/vstpresetfile.h\"\n#include \"public.sdk/source/common/memorystream.h\"\n\nELK_POP_WARNING\n\n#include \"twine/twine.h\"\n\n#include \"elklog/static_logger.h\"\n\n#include \"vst3x_wrapper.h\"\n#include \"vst3x_file_utils.h\"\n#include \"library/event.h\"\n\nnamespace sushi::internal::vst3 {\n\nconstexpr int VST_NAME_BUFFER_SIZE = 128;\n\nconstexpr uint32_t SUSHI_HOST_TIME_CAPABILITIES = Steinberg::Vst::ProcessContext::kSystemTimeValid &\n                                                  Steinberg::Vst::ProcessContext::kContTimeValid &\n                                                  Steinberg::Vst::ProcessContext::kBarPositionValid &\n                                                  Steinberg::Vst::ProcessContext::kTempoValid &\n                                                  Steinberg::Vst::ProcessContext::kTimeSigValid;\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"vst3\");\n\n// Convert a Steinberg 128 char unicode string to 8 bit ascii std::string\nstd::string to_ascii_str(Steinberg::Vst::String128 wchar_buffer)\n{\n    char char_buf[128] = {};\n    Steinberg::UString128 str(wchar_buffer, 128);\n    str.toAscii(char_buf, VST_NAME_BUFFER_SIZE);\n    return {char_buf};\n}\n\nvoid Vst3xWrapper::_cleanup()\n{\n    if (_instance.component())\n    {\n        set_enabled(false);\n    }\n\n    Vst3xRtState* state;\n    while (_state_change_queue.pop(state))\n    {\n        delete state;\n    }\n}\n\nVst3xWrapper::~Vst3xWrapper()\n{\n    ELKLOG_LOG_DEBUG(\"Unloading plugin {}\", this->name());\n    _cleanup();\n}\n\nProcessorReturnCode Vst3xWrapper::init(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    bool loaded = _instance.load_plugin(_host_control.to_absolute_path(_plugin_load_path),\n                                        _plugin_load_name,\n                                        &_component_handler);\n    if (!loaded)\n    {\n        _cleanup();\n        return ProcessorReturnCode::PLUGIN_LOAD_ERROR;\n    }\n    set_name(_instance.name());\n    set_label(_instance.name());\n\n    return _setup();\n}\n\nvoid Vst3xWrapper::configure(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    bool reset_enabled = enabled();\n    if (reset_enabled)\n    {\n        set_enabled(false);\n    }\n    if (!_setup_processing())\n    {\n        // TODO how to handle this?\n        ELKLOG_LOG_ERROR(\"Error setting sample rate to {}\", sample_rate);\n    }\n    if (reset_enabled)\n    {\n        set_enabled(true);\n    }\n}\n\nvoid Vst3xWrapper::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n        case RtEventType::FLOAT_PARAMETER_CHANGE:\n        {\n            auto typed_event = event.parameter_change_event();\n            _add_parameter_change(typed_event->param_id(), typed_event->value(), typed_event->sample_offset());\n            _parameter_update_queue.push({typed_event->param_id(), typed_event->value()});\n            _notify_parameter_change = true;\n            break;\n        }\n        case RtEventType::NOTE_ON:\n        {\n            auto vst_event = convert_note_on_event(event.keyboard_event());\n            _in_event_list.addEvent(vst_event);\n            break;\n        }\n        case RtEventType::NOTE_OFF:\n        {\n            auto vst_event = convert_note_off_event(event.keyboard_event());\n            _in_event_list.addEvent(vst_event);\n            break;\n        }\n        case RtEventType::NOTE_AFTERTOUCH:\n        {\n            auto vst_event = convert_aftertouch_event(event.keyboard_event());\n            _in_event_list.addEvent(vst_event);\n            break;\n        }\n        case RtEventType::MODULATION:\n        {\n            if (_mod_wheel_parameter.supported)\n            {\n                auto typed_event = event.keyboard_common_event();\n                _add_parameter_change(_mod_wheel_parameter.id, typed_event->value(), typed_event->sample_offset());\n            }\n            break;\n        }\n        case RtEventType::PITCH_BEND:\n        {\n            if (_pitch_bend_parameter.supported)\n            {\n                auto typed_event = event.keyboard_common_event();\n                float pb_value = (typed_event->value() + 1.0f) * 0.5f;\n                _add_parameter_change(_pitch_bend_parameter.id, pb_value, typed_event->sample_offset());\n            }\n            break;\n        }\n        case RtEventType::AFTERTOUCH:\n        {\n            if (_aftertouch_parameter.supported)\n            {\n                auto typed_event = event.keyboard_common_event();\n                _add_parameter_change(_aftertouch_parameter.id, typed_event->value(), typed_event->sample_offset());\n            }\n            break;\n        }\n        case RtEventType::STRING_PROPERTY_CHANGE:\n        {\n            auto typed_event = event.property_change_event();\n            auto ext = _instance.processor_extension();\n            if (ext)\n            {\n                ext->propertyValueChanged(typed_event->param_id(),\n                                          {typed_event->value()->c_str(), (int)typed_event->value()->size()});\n            }\n            async_delete(typed_event->deletable_value());\n            break;\n        }\n        case RtEventType::ASYNC_WORK_NOTIFICATION:\n        {\n            auto typed_event = event.async_work_completion_event();\n            auto ext = _instance.processor_extension();\n            if (ext)\n            {\n                ext->asyncWorkCompleted(typed_event->sending_event_id(), typed_event->return_status());\n            }\n            break;\n        }\n        case RtEventType::SET_BYPASS:\n        {\n            bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n            _bypass_manager.set_bypass(bypassed, _sample_rate);\n            break;\n        }\n        case RtEventType::SET_STATE:\n        {\n            auto state = event.processor_state_event()->state();\n            _set_state_rt(static_cast<Vst3xRtState*>(state));\n            break;\n        }\n        default:\n            break;\n    }\n}\n\nvoid Vst3xWrapper::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    Vst3xRtState* state_update = nullptr;\n\n    if (_bypass_parameter.supported == false && _bypass_manager.should_process() == false)\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n    else\n    {\n        _fill_processing_context();\n\n        if (_state_change_queue.pop(state_update))\n        {\n            _process_data.inputParameterChanges = state_update;\n        }\n        _process_data.assign_buffers(in_buffer, out_buffer, _current_input_channels, _current_output_channels);\n        _instance.processor()->process(_process_data);\n        if (_bypass_parameter.supported == false && _bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer, _current_input_channels, _current_output_channels);\n        }\n        _forward_events(_process_data);\n        _forward_params(_process_data);\n    }\n\n    if (_notify_parameter_change)\n    {\n        request_non_rt_task(parameter_update_callback);\n        _notify_parameter_change = false;\n    }\n\n    if (state_update)\n    {\n        _process_data.inputParameterChanges = &_in_parameter_changes;\n        async_delete(state_update);\n        notify_state_change_rt();\n    }\n    _process_data.clear();\n}\n\nvoid Vst3xWrapper::set_channels(int inputs, int outputs)\n{\n    Processor::set_channels(inputs, outputs);\n    _setup_channels();\n}\n\nvoid Vst3xWrapper::set_enabled(bool enabled)\n{\n    if (enabled == _enabled)\n    {\n        return;\n    }\n\n    // Activate component first, then enable processing, but deactivate in reverse order\n    // See: https://developer.steinberg.help/display/VST/Audio+Processor+Call+Sequence\n    if (enabled)\n    {\n        _instance.component()->setActive(true);\n        _instance.processor()->setProcessing(true);\n    }\n    else\n    {\n        _instance.processor()->setProcessing(false);\n        _instance.component()->setActive(false);\n    }\n    Processor::set_enabled(enabled);\n}\n\nvoid Vst3xWrapper::set_bypassed(bool bypassed)\n{\n    assert(twine::is_current_thread_realtime() == false);\n    if (_bypass_parameter.supported)\n    {\n        _host_control.post_event(std::make_unique<ParameterChangeEvent>(ParameterChangeEvent::Subtype::FLOAT_PARAMETER_CHANGE,\n                                                                        this->id(),\n                                                                        _bypass_parameter.id,\n                                                                        bypassed? 1.0f : 0,\n                                                                        IMMEDIATE_PROCESS));\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n    }\n    else\n    {\n        _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(),\n                                                                           bypassed,\n                                                                           IMMEDIATE_PROCESS));\n    }\n}\n\nbool Vst3xWrapper::bypassed() const\n{\n    if (_bypass_parameter.supported)\n    {\n        float value;\n        std::tie(std::ignore, value) = this->parameter_value(_bypass_parameter.id);\n        return value > 0.5;\n    }\n    return _bypass_manager.bypassed();\n}\n\nconst ParameterDescriptor* Vst3xWrapper::parameter_from_id(ObjectId id) const\n{\n    auto descriptor = _parameters_by_vst3_id.find(static_cast<Steinberg::Vst::ParamID>(id));\n    if (descriptor !=  _parameters_by_vst3_id.end())\n    {\n        return descriptor->second;\n    }\n    return nullptr;\n}\n\nstd::pair<ProcessorReturnCode, float> Vst3xWrapper::parameter_value(ObjectId parameter_id) const\n{\n    /* Always returns OK as the default vst3 implementation just returns 0 for invalid parameter ids */\n    auto controller = const_cast<PluginInstance*>(&_instance)->controller();\n    auto value = controller->getParamNormalized(parameter_id);\n    return {ProcessorReturnCode::OK, static_cast<float>(value)};\n}\n\nstd::pair<ProcessorReturnCode, float> Vst3xWrapper::parameter_value_in_domain(ObjectId parameter_id) const\n{\n    /* Always returns OK as the default vst3 implementation just returns 0 for invalid parameter ids */\n    auto controller = const_cast<PluginInstance*>(&_instance)->controller();\n    auto value = controller->normalizedParamToPlain(parameter_id, controller->getParamNormalized(parameter_id));\n    return {ProcessorReturnCode::OK, static_cast<float>(value)};\n}\n\nstd::pair<ProcessorReturnCode, std::string> Vst3xWrapper::parameter_value_formatted(ObjectId parameter_id) const\n{\n    auto controller = const_cast<PluginInstance*>(&_instance)->controller();\n    auto value = controller->getParamNormalized(parameter_id);\n    Steinberg::Vst::String128 buffer = {};\n    auto res = controller->getParamStringByValue(parameter_id, value, buffer);\n    if (res == Steinberg::kResultOk)\n    {\n        return {ProcessorReturnCode::OK, to_ascii_str(buffer)};\n    }\n    return {ProcessorReturnCode::PARAMETER_NOT_FOUND, \"\"};\n}\n\nint Vst3xWrapper::current_program() const\n{\n    if (_supports_programs)\n    {\n        return _current_program;\n    }\n    return 0;\n}\n\nstd::pair<ProcessorReturnCode, std::string> Vst3xWrapper::property_value(ObjectId property_id) const\n{\n    auto controller_ext = const_cast<PluginInstance&>(_instance).controller_extension();\n    if (controller_ext)\n    {\n        elk::PropertyValue value{nullptr ,0};\n        auto res = controller_ext->getPropertyValue(static_cast<Steinberg::int32>(property_id), value);\n        if (res == Steinberg::kResultOk && value.value)\n        {\n            return {ProcessorReturnCode::OK,\n                    std::string(value.value, std::clamp(value.length, 0, elk::STRING_PROPERTY_DEFAULT_LENGTH))};\n        }\n    }\n    return {ProcessorReturnCode::PARAMETER_NOT_FOUND, \"\"};\n}\n\nProcessorReturnCode Vst3xWrapper::set_property_value(ObjectId property_id, const std::string& value)\n{\n    auto controller_ext = const_cast<PluginInstance&>(_instance).controller_extension();\n    auto config = _property_configs.find(property_id);\n    if (controller_ext && config != _property_configs.end())\n    {\n        if (!config->second.automatable)\n        {\n            return ProcessorReturnCode::UNSUPPORTED_OPERATION;\n        }\n\n        elk::PropertyValue property_value{const_cast<char*>(value.c_str()),\n                                          std::clamp<int>(value.size(), 0, elk::STRING_PROPERTY_DEFAULT_LENGTH)};\n        auto res = controller_ext->setPropertyValue(static_cast<Steinberg::int32>(property_id), property_value);\n        if (res == Steinberg::kResultOk)\n        {\n            // Non rt thread notification (will be output through OSC, gRPC, etc)\n            _host_control.post_event(std::make_unique<PropertyChangeNotificationEvent>(this->id(),\n                                                                                       property_id,\n                                                                                       value,\n                                                                                       IMMEDIATE_PROCESS));\n            // Audio thread notification\n            if (config->second.audio_thread_notification)\n            {\n                _host_control.post_event(std::make_unique<StringPropertyEvent>(this->id(),\n                                                                               property_id,\n                                                                               value,\n                                                                               IMMEDIATE_PROCESS));\n            }\n            return ProcessorReturnCode::OK;\n        }\n    }\n    return ProcessorReturnCode::PARAMETER_NOT_FOUND;\n}\n\nstd::string Vst3xWrapper::current_program_name() const\n{\n    return program_name(_current_program).second;\n}\n\nstd::pair<ProcessorReturnCode, std::string> Vst3xWrapper::program_name(int program) const\n{\n    if (_supports_programs && _internal_programs)\n    {\n        ELKLOG_LOG_INFO(\"Program name {}\", program);\n        auto mutable_unit = const_cast<PluginInstance*>(&_instance)->unit_info();\n        Steinberg::Vst::String128 buffer;\n        auto res = mutable_unit->getProgramName(_main_program_list_id, program, buffer);\n        if (res == Steinberg::kResultOk)\n        {\n            ELKLOG_LOG_INFO(\"Program name returned error {}\", res);\n            return {ProcessorReturnCode::OK, to_ascii_str(buffer)};\n        }\n    }\n    else if (_supports_programs && _file_based_programs && program < static_cast<int>(_program_files.size()))\n    {\n        return {ProcessorReturnCode::OK, extract_preset_name(_program_files[program].string())};\n    }\n    ELKLOG_LOG_INFO(\"Set program name failed\");\n    return {ProcessorReturnCode::UNSUPPORTED_OPERATION, \"\"};\n}\n\nstd::pair<ProcessorReturnCode, std::vector<std::string>> Vst3xWrapper::all_program_names() const\n{\n    if (_supports_programs)\n    {\n        ELKLOG_LOG_INFO(\"all Program names\");\n        std::vector<std::string> programs;\n        auto mutable_unit = const_cast<PluginInstance*>(&_instance)->unit_info();\n        for (int i = 0; i < _program_count; ++i)\n        {\n            if (_internal_programs)\n            {\n                Steinberg::Vst::String128 buffer;\n                auto res = mutable_unit->getProgramName(_main_program_list_id, i, buffer);\n                if (res == Steinberg::kResultOk)\n                {\n                    programs.push_back(to_ascii_str(buffer));\n                } else\n                {\n                    ELKLOG_LOG_INFO(\"Program name returned error {} on {}\", res, i);\n                    break;\n                }\n            }\n            else if (_file_based_programs)\n            {\n                programs.push_back(extract_preset_name(_program_files[i].string()));\n            }\n        }\n        ELKLOG_LOG_INFO(\"Return list with {} programs\", programs.size());\n        return {ProcessorReturnCode::OK, programs};\n    }\n    ELKLOG_LOG_INFO(\"All program names failed\");\n    return {ProcessorReturnCode::UNSUPPORTED_OPERATION, std::vector<std::string>()};\n}\n\nProcessorReturnCode Vst3xWrapper::set_program(int program)\n{\n    if (!_supports_programs || _program_count == 0)\n    {\n        return ProcessorReturnCode::UNSUPPORTED_OPERATION;\n    }\n    if (_internal_programs)\n    {\n        float normalised_program_id = static_cast<float>(program) / static_cast<float>(_program_count);\n        auto event = std::make_unique<ParameterChangeEvent>(ParameterChangeEvent::Subtype::FLOAT_PARAMETER_CHANGE,\n                                                            this->id(),\n                                                            _program_change_parameter.id,\n                                                            normalised_program_id,\n                                                            IMMEDIATE_PROCESS);\n        event->set_completion_cb(Vst3xWrapper::program_change_callback, this);\n        _host_control.post_event(std::move(event));\n        ELKLOG_LOG_INFO(\"Set program {}, {}, {}\", program, normalised_program_id, _program_change_parameter.id);\n\n        return ProcessorReturnCode::OK;\n    }\n    else if (_file_based_programs && program < static_cast<int>(_program_files.size()))\n    {\n        ELKLOG_LOG_INFO(\"Loading file based preset\");\n        Steinberg::OPtr<Steinberg::IBStream> stream(Steinberg::Vst::FileStream::open(_program_files[program].string().c_str(), \"rb\"));\n        if (stream == nullptr)\n        {\n            ELKLOG_LOG_INFO(\"Failed to load file {}\", _program_files[program].string().c_str());\n            return ProcessorReturnCode::ERROR;\n        }\n        Steinberg::Vst::PresetFile preset_file(stream);\n        preset_file.readChunkList();\n\n        bool res = preset_file.restoreControllerState(_instance.controller());\n        res &= preset_file.restoreComponentState(_instance.component());\n        // Notify the processor of the update with an idle message. This was specific\n        // to Retrologue and not part of the Vst3 standard, so we might remove it eventually\n        Steinberg::Vst::HostMessage message;\n        message.setMessageID(\"idle\");\n        if (_instance.notify_processor(&message) == false)\n        {\n            ELKLOG_LOG_ERROR(\"Idle message returned error\");\n        }\n        if (res)\n        {\n            _current_program = program;\n            return ProcessorReturnCode::OK;\n        } else\n        {\n            ELKLOG_LOG_INFO(\"restore state returned error\");\n        }\n    }\n    ELKLOG_LOG_INFO(\"Error in program change\");\n    return ProcessorReturnCode::ERROR;\n}\n\nProcessorReturnCode Vst3xWrapper::set_state(ProcessorState* state, bool realtime_running)\n{\n    if (state->has_binary_data())\n    {\n        _set_binary_state(state->binary_data());\n        return ProcessorReturnCode::OK;\n    }\n\n    std::unique_ptr<Vst3xRtState> rt_state;\n    if (realtime_running)\n    {\n        rt_state = std::make_unique<Vst3xRtState>(*state);\n    }\n\n    if (state->program().has_value())\n    {\n        _set_program_state(*state->program(), rt_state.get(), realtime_running);\n    }\n\n    if (state->bypassed().has_value())\n    {\n        _set_bypass_state(*state->bypassed(), rt_state.get(), realtime_running);\n    }\n\n    for (const auto& parameter : state->parameters())\n    {\n        int id = parameter.first;\n        float value = parameter.second;\n        _instance.controller()->setParamNormalized(id, value);\n        if (realtime_running == false)\n        {\n            _add_parameter_change(id, value, 0);\n        }\n    }\n\n    if (realtime_running)\n    {\n        _host_control.post_event(std::make_unique<RtStateEvent>(this->id(), std::move(rt_state), IMMEDIATE_PROCESS));\n    }\n    else\n    {\n        _host_control.post_event(std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_UPDATED,\n                                                                               this->id(), 0, IMMEDIATE_PROCESS));\n    }\n\n    return ProcessorReturnCode::OK;\n}\n\n\nProcessorState Vst3xWrapper::save_state() const\n{\n    ProcessorState state;\n    Steinberg::MemoryStream stream;\n    if (_instance.component()->getState (&stream) == Steinberg::kResultTrue)\n    {\n        auto data_size = stream.getSize();\n        auto data = reinterpret_cast<std::byte*>(stream.getData());\n        state.set_binary_data(std::vector<std::byte>(data, data + data_size));\n    }\n    else\n    {\n        ELKLOG_LOG_WARNING(\"Failed to get component state\");\n    }\n    return state;\n}\n\nPluginInfo Vst3xWrapper::info() const\n{\n    PluginInfo info;\n    info.type = PluginType::VST3X;\n    info.uid  = _plugin_load_name;\n    info.path = _plugin_load_path;\n    return info;\n}\n\nProcessorReturnCode Vst3xWrapper::_setup()\n{\n    if (!_setup_audio_buses() || !_setup_event_buses())\n    {\n        return ProcessorReturnCode::PLUGIN_INIT_ERROR;\n    }\n\n    if (!_sync_processor_to_controller())\n    {\n        ELKLOG_LOG_WARNING(\"failed to sync controller\");\n    }\n\n    if (!_setup_processing())\n    {\n        return ProcessorReturnCode::PLUGIN_INIT_ERROR;\n    }\n\n    _register_parameters();\n    _register_properties();\n\n    if (!_setup_internal_program_handling())\n    {\n        _setup_file_program_handling();\n    }\n    return ProcessorReturnCode::OK;\n}\n\nbool Vst3xWrapper::_register_parameters()\n{\n    int param_count = _instance.controller()->getParameterCount();\n    _in_parameter_changes.setMaxParameters(param_count);\n    _out_parameter_changes.setMaxParameters(param_count);\n\n    for (int i = 0; i < param_count; ++i)\n    {\n        Steinberg::Vst::ParameterInfo info;\n        auto res = _instance.controller()->getParameterInfo(i, info);\n        if (res == Steinberg::kResultOk)\n        {\n            /* Vst3 uses a model where parameters are indexed by an integer from 0 to\n             * getParameterCount() - 1 (just like Vst2.4). But in addition, each parameter\n             * also has a 32-bit integer id which is arbitrarily assigned.\n             *\n             * When doing real time parameter updates, the parameters must be accessed using this\n             * id and not its index. Hence, the id in the registered ParameterDescriptors\n             * store this id and not the index in the processor array like it does for the Vst2\n             * wrapper and internal plugins. Hopefully that doesn't cause any issues. */\n            auto param_name = to_ascii_str(info.title);\n            auto param_unit = to_ascii_str(info.units);\n            bool automatable = info.flags & Steinberg::Vst::ParameterInfo::kCanAutomate;\n            bool read_only = info.flags & Steinberg::Vst::ParameterInfo::kIsReadOnly;\n\n            auto direction = (automatable && !read_only) ? Direction::AUTOMATABLE : Direction::OUTPUT;\n\n            if (info.flags & Steinberg::Vst::ParameterInfo::kIsBypass)\n            {\n                _bypass_parameter.id = info.id;\n                _bypass_parameter.supported = true;\n                ELKLOG_LOG_INFO(\"Plugin supports soft bypass\");\n            }\n            else if (info.flags & Steinberg::Vst::ParameterInfo::kIsProgramChange &&\n                     _program_change_parameter.supported == false)\n            {\n                /* For now, we only support 1 program change parameter, and we're counting on the\n                 * first one to be the global one. Multitimbral instruments can have multiple\n                 * program change parameters, but we'll have to look into how to support that. */\n                _program_change_parameter.id = info.id;\n                _program_change_parameter.supported = true;\n                ELKLOG_LOG_INFO(\"We have a program change parameter at {}\", info.id);\n            }\n            else if (info.stepCount > 0 &&\n                     register_parameter(new IntParameterDescriptor(_make_unique_parameter_name(param_name),\n                                                                   param_name,\n                                                                   param_unit,\n                                                                   0,\n                                                                   info.stepCount,\n                                                                   direction,\n                                                                   nullptr),\n                                        info.id))\n            {\n                ELKLOG_LOG_INFO(\"Registered INT parameter {}, id {}\", param_name, info.id);\n            }\n            else if (register_parameter(new FloatParameterDescriptor(_make_unique_parameter_name(param_name),\n                                                                     param_name,\n                                                                     param_unit,\n                                                                     0,\n                                                                     1,\n                                                                     direction,\n                                                                     nullptr),\n                                        info.id))\n            {\n                ELKLOG_LOG_INFO(\"Registered parameter {}, id {}\", param_name, info.id);\n            }\n            else\n            {\n                ELKLOG_LOG_INFO(\"Error registering parameter {}.\", param_name);\n            }\n        }\n    }\n\n    // Create a \"backwards map\" from Vst3 parameter ids to parameter indices\n    for (auto param : this->all_parameters())\n    {\n        _parameters_by_vst3_id[param->id()] = param;\n    }\n\n    /* Steinberg decided not support standard midi, nor provide special events for common\n     * controller (Pitch bend, mod wheel, etc.) instead these are exposed as regular\n     * parameters, and we can query the plugin for what 'default' midi cc:s these parameters\n     * would be mapped to if the plugin was able to handle native midi.\n     * So we query the plugin for this and if that's the case, store the id:s of these\n     * 'special' parameters, so we can map PB and Mod events to them.\n     * Currently, we don't hide these parameters, unlike the bypass parameter, so they can\n     * still be controlled via OSC or other controllers. */\n    if (_instance.midi_mapper())\n    {\n        if (_instance.midi_mapper()->getMidiControllerAssignment(0, 0, Steinberg::Vst::kCtrlModWheel,\n                                                     _mod_wheel_parameter.id) == Steinberg::kResultOk)\n        {\n            ELKLOG_LOG_INFO(\"Plugin supports mod wheel parameter mapping\");\n            _mod_wheel_parameter.supported = true;\n        }\n        if (_instance.midi_mapper()->getMidiControllerAssignment(0, 0, Steinberg::Vst::kPitchBend,\n                                                     _pitch_bend_parameter.id) == Steinberg::kResultOk)\n        {\n            ELKLOG_LOG_INFO(\"Plugin supports pitch bend parameter mapping\");\n            _pitch_bend_parameter.supported = true;\n        }\n        if (_instance.midi_mapper()->getMidiControllerAssignment(0, 0, Steinberg::Vst::kAfterTouch,\n                                                     _aftertouch_parameter.id) == Steinberg::kResultOk)\n        {\n            ELKLOG_LOG_INFO(\"Plugin supports aftertouch parameter mapping\");\n            _aftertouch_parameter.supported = true;\n        }\n    }\n\n    return true;\n}\n\nbool Vst3xWrapper::_register_properties()\n{\n    /* String parameters/properties are not supported in Vst3, they can only be used if the plugin\n     * implements our extension api. The IDs of these string extension properties must still not\n     * conflict with any existing float parameters */\n    auto controller = _instance.controller_extension();\n    if (!controller)\n    {\n        return false;\n    }\n    for (int i = 0 ; i < controller->getPropertyCount(); ++i)\n    {\n        elk::PropertyInfo info;\n        auto res = controller->getPropertyInfo(i, info);\n        if (res == Steinberg::kResultOk)\n        {\n            auto name = to_ascii_str(info.name);\n            auto label = to_ascii_str(info.label);\n            bool read_only = info.flags & elk::PropertyInfo::kIsReadOnly;\n            bool audio_thread_notification = info.flags & elk::PropertyInfo::kAudioThreadNotify;\n\n            auto direction = read_only ? Direction::OUTPUT : Direction::AUTOMATABLE;\n\n            auto param = new StringPropertyDescriptor(_make_unique_parameter_name(name), label, \"\", direction);\n\n            if (!this->register_parameter(param, info.id))\n            {\n                ELKLOG_LOG_WARNING(\"Failed to register string property \\\"{}\\\"\", name);\n                delete param;\n                continue;\n            }\n\n            PropertyInfo config = {.automatable = !read_only, .audio_thread_notification = audio_thread_notification};\n            _property_configs[info.id] = config;\n            _parameters_by_vst3_id[param->id()] = param;\n            ELKLOG_LOG_INFO(\"Registered string property \\\"{}\\\"\\\" {} \", name, read_only? \"as read only\" : \"\");\n        }\n    }\n    return true;\n}\n\n\nbool Vst3xWrapper::_setup_audio_buses()\n{\n    int input_audio_buses = _instance.component()->getBusCount(Steinberg::Vst::MediaTypes::kAudio, Steinberg::Vst::BusDirections::kInput);\n    int output_audio_buses = _instance.component()->getBusCount(Steinberg::Vst::MediaTypes::kAudio, Steinberg::Vst::BusDirections::kOutput);\n    ELKLOG_LOG_INFO(\"Plugin has {} audio input buffers and {} audio output buffers\", input_audio_buses, output_audio_buses);\n    if (output_audio_buses == 0)\n    {\n        return false;\n    }\n    _max_input_channels = 0;\n    _max_output_channels = 0;\n    /* Setup 1 main output bus and 1 main input bus (if available) */\n    Steinberg::Vst::BusInfo info;\n    for (int i = 0; i < input_audio_buses; ++i)\n    {\n        auto res = _instance.component()->getBusInfo(Steinberg::Vst::MediaTypes::kAudio,\n                                                     Steinberg::Vst::BusDirections::kInput, i, info);\n        if (res == Steinberg::kResultOk && info.busType == Steinberg::Vst::BusTypes::kMain) // Then use this one\n        {\n            _max_input_channels = info.channelCount;\n            res = _instance.component()->activateBus(Steinberg::Vst::MediaTypes::kAudio,\n                                                     Steinberg::Vst::BusDirections::kInput, i, Steinberg::TBool(true));\n            if (res != Steinberg::kResultOk)\n            {\n                ELKLOG_LOG_ERROR(\"Failed to activate plugin input bus {}\", i);\n                return false;\n            }\n            break;\n        }\n    }\n    for (int i = 0; i < output_audio_buses; ++i)\n    {\n        auto res = _instance.component()->getBusInfo(Steinberg::Vst::MediaTypes::kAudio,\n                                                     Steinberg::Vst::BusDirections::kOutput, i, info);\n        if (res == Steinberg::kResultOk && info.busType == Steinberg::Vst::BusTypes::kMain) // Then use this one\n        {\n            _max_output_channels = info.channelCount;\n            res = _instance.component()->activateBus(Steinberg::Vst::MediaTypes::kAudio,\n                                                     Steinberg::Vst::BusDirections::kOutput, i, Steinberg::TBool(true));\n            if (res != Steinberg::kResultOk)\n            {\n                ELKLOG_LOG_ERROR(\"Failed to activate plugin output bus {}\", i);\n                return false;\n            }\n            break;\n        }\n    }\n    ELKLOG_LOG_INFO(\"Vst3 wrapper ({}) has {} inputs and {} outputs\", this->name(), _max_input_channels, _max_output_channels);\n    return true;\n}\n\nbool Vst3xWrapper::_setup_event_buses()\n{\n    int input_buses = _instance.component()->getBusCount(Steinberg::Vst::MediaTypes::kEvent, Steinberg::Vst::BusDirections::kInput);\n    int output_buses = _instance.component()->getBusCount(Steinberg::Vst::MediaTypes::kEvent, Steinberg::Vst::BusDirections::kOutput);\n    ELKLOG_LOG_INFO(\"Plugin has {} event input buffers and {} event output buffers\", input_buses, output_buses);\n    /* Try to activate all buses here */\n    for (int i = 0; i < input_buses; ++i)\n    {\n        auto res = _instance.component()->activateBus(Steinberg::Vst::MediaTypes::kEvent,\n                                                     Steinberg::Vst::BusDirections::kInput, i, Steinberg::TBool(true));\n        if (res != Steinberg::kResultOk)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to activate plugin input event bus {}\", i);\n            return false;\n        }\n    }\n    for (int i = 0; i < output_buses; ++i)\n    {\n        auto res = _instance.component()->activateBus(Steinberg::Vst::MediaTypes::kEvent,\n                                                      Steinberg::Vst::BusDirections::kInput, i, Steinberg::TBool(true));\n        if (res != Steinberg::kResultOk)\n        {\n            ELKLOG_LOG_ERROR(\"Failed to activate plugin output event bus {}\", i);\n            return false;\n        }\n    }\n    return true;\n}\n\nbool Vst3xWrapper::_setup_channels()\n{\n    ELKLOG_LOG_INFO(\"Vst3 wrapper ({}) setting up {} inputs and {} outputs\", this->name(), _current_input_channels, _current_output_channels);\n    Steinberg::Vst::SpeakerArrangement input_arr = speaker_arr_from_channels(_current_input_channels);\n    Steinberg::Vst::SpeakerArrangement output_arr = speaker_arr_from_channels(_current_output_channels);\n\n    /* numIns and numOuts refer to the number of buses, not channels, the docs are very vague on this point */\n    auto res = _instance.processor()->setBusArrangements(&input_arr, (_max_input_channels == 0)? 0:1, &output_arr, 1);\n    if (res != Steinberg::kResultOk)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to set a valid channel arrangement\");\n        return false;\n    }\n    return true;\n}\n\nbool Vst3xWrapper::_setup_processing()\n{\n    _process_data.processContext->sampleRate = _sample_rate;\n    Steinberg::Vst::ProcessSetup setup;\n    setup.maxSamplesPerBlock = AUDIO_CHUNK_SIZE;\n    setup.processMode = Steinberg::Vst::ProcessModes::kRealtime;\n    setup.sampleRate = _sample_rate;\n    setup.symbolicSampleSize = Steinberg::Vst::SymbolicSampleSizes::kSample32;\n    auto res = _instance.processor()->setupProcessing(setup);\n    if (res != Steinberg::kResultOk)\n    {\n        ELKLOG_LOG_ERROR(\"Error setting up processing, error code: {}\", res);\n        return false;\n    }\n    return true;\n}\n\nbool Vst3xWrapper::_setup_internal_program_handling()\n{\n    if (_instance.unit_info() == nullptr || _program_change_parameter.supported == false)\n    {\n        ELKLOG_LOG_INFO(\"No unit info or program change parameter\");\n        return false;\n    }\n    if (_instance.unit_info()->getProgramListCount() == 0)\n    {\n        ELKLOG_LOG_INFO(\"ProgramListCount is 0\");\n        return false;\n    }\n    _main_program_list_id = 0;\n    Steinberg::Vst::UnitInfo info;\n    auto res = _instance.unit_info()->getUnitInfo(Steinberg::Vst::kRootUnitId, info);\n    if (res == Steinberg::kResultOk && info.programListId != Steinberg::Vst::kNoProgramListId)\n    {\n        ELKLOG_LOG_INFO(\"Program list id {}\", info.programListId);\n        _main_program_list_id = info.programListId;\n    }\n    /* This is most likely 0, but query and store for good measure as we might want\n     * to support multiple program lists in the future */\n    Steinberg::Vst::ProgramListInfo list_info;\n    res = _instance.unit_info()->getProgramListInfo(Steinberg::Vst::kRootUnitId, list_info);\n    if (res == Steinberg::kResultOk)\n    {\n        _supports_programs = true;\n        _program_count = list_info.programCount;\n        ELKLOG_LOG_INFO(\"Plugin supports internal programs, program count: {}\", _program_count);\n        _internal_programs = true;\n        return true;\n    }\n    ELKLOG_LOG_INFO(\"No program list info, returned {}\", res);\n    return false;\n}\n\nbool Vst3xWrapper::_setup_file_program_handling()\n{\n    _program_files = scan_for_presets(make_safe_folder_name(_instance.name()), make_safe_folder_name(_instance.vendor()));\n    if (!_program_files.empty())\n    {\n        _supports_programs = true;\n        _file_based_programs = true;\n        _program_count = static_cast<int>(_program_files.size());\n        ELKLOG_LOG_INFO(\"Using external file programs, {} program files found\", _program_files.size());\n        return true;\n    }\n    return false;\n}\n\nvoid Vst3xWrapper::_forward_events(Steinberg::Vst::ProcessData& data)\n{\n    int event_count = data.outputEvents->getEventCount();\n    for (int i = 0; i < event_count; ++i)\n    {\n        Steinberg::Vst::Event vst_event;\n        if (data.outputEvents->getEvent(i, vst_event) == Steinberg::kResultOk)\n        {\n            switch (vst_event.type)\n            {\n                case Steinberg::Vst::Event::EventTypes::kNoteOnEvent:\n                    if (maybe_output_gate_event(vst_event.noteOn.channel, vst_event.noteOn.pitch, true) == false)\n                    {\n                        output_event(RtEvent::make_note_on_event(0, vst_event.sampleOffset,\n                                                                    vst_event.noteOn.channel,\n                                                                    vst_event.noteOn.pitch,\n                                                                    vst_event.noteOn.velocity));\n                    }\n                    break;\n\n                case Steinberg::Vst::Event::EventTypes::kNoteOffEvent:\n                    if (maybe_output_gate_event(vst_event.noteOn.channel, vst_event.noteOn.pitch, false) == false)\n                    {\n                        output_event(RtEvent::make_note_off_event(0, vst_event.sampleOffset,\n                                                                     vst_event.noteOff.channel,\n                                                                     vst_event.noteOff.pitch,\n                                                                     vst_event.noteOff.velocity));\n                    }\n                    break;\n\n                case Steinberg::Vst::Event::EventTypes::kPolyPressureEvent:\n                    output_event(RtEvent::make_note_aftertouch_event(0, vst_event.sampleOffset,\n                                                                     vst_event.polyPressure.channel,\n                                                                     vst_event.polyPressure.pitch,\n                                                                     vst_event.polyPressure.pressure));\n                    break;\n\n                default:\n                    break;\n            }\n        }\n    }\n}\n\nvoid Vst3xWrapper::_forward_params(Steinberg::Vst::ProcessData& data)\n{\n    int param_count = data.outputParameterChanges->getParameterCount();\n    for (int i = 0; i < param_count; ++i)\n    {\n        auto queue = data.outputParameterChanges->getParameterData(i);\n        auto id = queue->getParameterId();\n        int points = queue->getPointCount();\n        if (points > 0)\n        {\n            double value;\n            int offset;\n            auto res = queue->getPoint(points - 1, offset, value);\n            if (res == Steinberg::kResultOk)\n            {\n                if (maybe_output_cv_value(id, value) == false)\n                {\n                    auto float_value = static_cast<float>(value);\n                    auto e = RtEvent::make_parameter_change_event(this->id(), 0, id, float_value);\n                    output_event(e);\n                    _parameter_update_queue.push({id, float_value});\n                    _notify_parameter_change = true;\n                }\n            }\n        }\n    }\n}\n\nvoid Vst3xWrapper::_fill_processing_context()\n{\n    auto transport = _host_control.transport();\n    auto context = _process_data.processContext;\n    *context = {};\n    auto ts = transport->time_signature();\n\n    context->state = SUSHI_HOST_TIME_CAPABILITIES | (transport->playing()? Steinberg::Vst::ProcessContext::kPlaying : 0);\n    context->sampleRate             = _sample_rate;\n    context->projectTimeSamples     = transport->current_samples();\n    context->systemTime             = std::chrono::nanoseconds(transport->current_process_time()).count();\n    context->continousTimeSamples   = transport->current_samples();\n    context->projectTimeMusic       = transport->current_beats();\n    context->barPositionMusic       = transport->current_bar_start_beats();\n    context->tempo                  = transport->current_tempo();\n    context->timeSigNumerator       = ts.numerator;\n    context->timeSigDenominator     = ts.denominator;\n}\n\ninline void Vst3xWrapper::_add_parameter_change(Steinberg::Vst::ParamID id, float value, int sample_offset)\n{\n    int index;\n    auto param_queue = _in_parameter_changes.addParameterData(id, index);\n    if (param_queue)\n    {\n        param_queue->addPoint(sample_offset, value, index);\n    }\n}\n\nvoid Vst3xWrapper::set_parameter_change(ObjectId param_id, float value)\n{\n    auto event = std::make_unique<ParameterChangeEvent>(ParameterChangeEvent::Subtype::FLOAT_PARAMETER_CHANGE,\n                                                        this->id(),\n                                                        param_id,\n                                                        value,\n                                                        IMMEDIATE_PROCESS);\n    _host_control.post_event(std::move(event));\n}\n\nbool Vst3xWrapper::_sync_processor_to_controller()\n{\n    Steinberg::MemoryStream stream;\n    if (_instance.component()->getState (&stream) == Steinberg::kResultTrue)\n    {\n        stream.seek(0, Steinberg::MemoryStream::kIBSeekSet, nullptr);\n        auto res = _instance.controller()->setComponentState (&stream);\n        return res == Steinberg::kResultTrue? true : false;\n    }\n    ELKLOG_LOG_WARNING(\"Failed to get state from processor\");\n    return false;\n}\n\nvoid Vst3xWrapper::_program_change_callback(Event* event, int status)\n{\n    if (status == EventStatus::HANDLED_OK)\n    {\n        auto typed_event = static_cast<ParameterChangeEvent*>(event);\n        _current_program = static_cast<int>(typed_event->float_value() * _program_count);\n        ELKLOG_LOG_INFO(\"Set program to {} completed, {}\", _current_program, typed_event->parameter_id());\n        _instance.controller()->setParamNormalized(_program_change_parameter.id, typed_event->float_value());\n        Steinberg::Vst::HostMessage message;\n        message.setMessageID(\"idle\");\n        if (_instance.notify_processor(&message) == false)\n        {\n            ELKLOG_LOG_ERROR(\"Idle message returned error\");\n        }\n        return;\n    }\n    ELKLOG_LOG_INFO(\"Set program failed with status: {}\", status);\n}\n\nint Vst3xWrapper::_parameter_update_callback(EventId /*id*/)\n{\n    ParameterUpdate update;\n    int res = 0;\n    while (_parameter_update_queue.pop(update))\n    {\n        res |= _instance.controller()->setParamNormalized(update.id, update.value);\n    }\n    return res == Steinberg::kResultOk? EventStatus::HANDLED_OK : EventStatus::ERROR;\n}\n\nvoid Vst3xWrapper::_set_program_state(int program_id, RtState* rt_state, bool realtime_running)\n{\n    if (_internal_programs && _program_change_parameter.supported)\n    {\n        float normalised_id = program_id / static_cast<float>(_program_count);\n        _instance.controller()->setParamNormalized(_program_change_parameter.id, normalised_id);\n        _current_program = program_id;\n        if (realtime_running)\n        {\n            rt_state->add_parameter_change(_program_change_parameter.id, normalised_id);\n        }\n        else\n        {\n            _add_parameter_change(_program_change_parameter.id, normalised_id, 0);\n        }\n    }\n    else // file based programs\n    {\n        this->set_program(program_id);\n    }\n}\n\nvoid Vst3xWrapper::_set_bypass_state(bool bypassed, RtState* rt_state, bool realtime_running)\n{\n    _bypass_manager.set_bypass(bypassed, _sample_rate);\n    if (_bypass_parameter.supported)\n    {\n        float float_bypass = bypassed ? 1.0f : 0.0f;\n        _instance.controller()->setParamNormalized(_bypass_parameter.id, float_bypass);\n        if (realtime_running)\n        {\n            rt_state->add_parameter_change(_bypass_parameter.id, float_bypass);\n        }\n        else\n        {\n            _add_parameter_change(_bypass_parameter.id, float_bypass, 0);\n        }\n    }\n}\n\nvoid Vst3xWrapper::_set_binary_state(std::vector<std::byte>& state)\n{\n    /* A primer on Vst3 states:\n     * Component.setState() sets the state of the audio processing part (param values, etc)\n     * Controller.setComponentState() sets the Controllers own mirror of parameter values.\n     * Controller.setState() only sets the editors internal state and is not called in sushi\n     *\n     * State functions are always called from a non-rt thread according to\n     * https://developer.steinberg.help/display/VST/Frequently+Asked+Questions */\n\n    auto stream = Steinberg::MemoryStream(reinterpret_cast<void*>(state.data()), state.size());\n    [[maybe_unused]] auto res = _instance.controller()->setComponentState(&stream);\n    ELKLOG_LOG_ERROR_IF(res != Steinberg::kResultOk, \"Failed to set component state on controller ({})\", res);\n\n    stream.seek(0, Steinberg::MemoryStream::kIBSeekSet, nullptr);\n    res = _instance.component()->setState(&stream);\n    ELKLOG_LOG_ERROR_IF(res , \"Failed to set component state ({})\", res);\n\n    auto event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_UPDATED,\n                                                               this->id(), 0, IMMEDIATE_PROCESS);\n    _host_control.post_event(std::move(event));\n}\n\nvoid Vst3xWrapper::_set_state_rt(Vst3xRtState* state)\n{\n    if (_state_change_queue.push(state) == false)\n    {\n        /* If the queue of parameter batches is full, ignore it and make sure the object doesn't leak.\n         * This should most likely never happen, even the case of 2 state changes in the same process\n         * call should be a very rare occasion */\n        async_delete(state);\n    }\n}\n\nEventId Vst3xWrapper::request_async_work(AsyncWorkCallback callback, void* data)\n{\n    auto event = RtEvent::make_async_work_event(callback, this->id(), data);\n    output_event(event);\n    return event.async_work_event()->event_id();\n}\nVst3xWrapper::Vst3xWrapper(HostControl host_control,\n                           const std::string& vst_plugin_path,\n                           const std::string& plugin_name) : Processor(host_control),\n                                                             _plugin_load_name(plugin_name),\n                                                             _plugin_load_path(vst_plugin_path),\n                                                             _component_handler(this, &_host_control),\n                                                             _host_app(this),\n                                                             _instance(&_host_app)\n\n{\n    _max_input_channels = VST_WRAPPER_MAX_N_CHANNELS;\n    _max_output_channels = VST_WRAPPER_MAX_N_CHANNELS;\n    _enabled = false;\n}\n\nSteinberg::Vst::SpeakerArrangement speaker_arr_from_channels(int channels)\n{\n    switch (channels)\n    {\n        case 0:\n            return Steinberg::Vst::SpeakerArr::kEmpty;\n        case 1:\n            return Steinberg::Vst::SpeakerArr::kMono;\n        case 2:\n            return Steinberg::Vst::SpeakerArr::kStereo;\n        case 3:\n            return Steinberg::Vst::SpeakerArr::k30Music;\n        case 4:\n            return Steinberg::Vst::SpeakerArr::k40Music;\n        case 5:\n            return Steinberg::Vst::SpeakerArr::k50;\n        case 6:\n            return Steinberg::Vst::SpeakerArr::k60Music;\n        case 7:\n            return Steinberg::Vst::SpeakerArr::k70Music;\n        default:\n            return Steinberg::Vst::SpeakerArr::k80Music;\n    }\n}\n\n} // end namespace sushi::internal::vst3\n"
  },
  {
    "path": "src/library/vst3x/vst3x_wrapper.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Wrapper for VST 3.x plugins.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_VST3X_WRAPPER_H\n#define SUSHI_VST3X_WRAPPER_H\n\n#include <map>\n#include <utility>\n#include <filesystem>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_EXTRA\nELK_DISABLE_DEPRECATED_DECLARATIONS\nELK_DISABLE_SHORTEN_64_TO_32\n\n#include \"pluginterfaces/base/ipluginbase.h\"\n#include \"public.sdk/source/vst/hosting/eventlist.h\"\n#include \"public.sdk/source/vst/hosting/parameterchanges.h\"\n\nELK_POP_WARNING\n\n#include \"fifo/circularfifo_memory_relaxed_aquire_release.h\"\n\n#include \"vst3x_host_app.h\"\n#include \"library/processor.h\"\n#include \"vst3x_utils.h\"\n\nnamespace sushi::internal::vst3 {\n\nconstexpr int VST_WRAPPER_NOTE_EVENT_QUEUE_SIZE = 256;\n// Maximum number of rt parameter changes passed on to the EditController\nconstexpr int PARAMETER_UPDATE_QUEUE_SIZE = 100;\n// Maximum number of cached state changes, as only 1 can be processes per audio process call\nconstexpr int STATE_CHANGE_QUEUE_SIZE = 10;\n\nclass Vst3xWrapperAccessor;\n\n/**\n * @brief internal wrapper class for loading VST plugins and make them accessible as Processor to the Engine.\n */\nclass Vst3xWrapper : public Processor\n{\npublic:\n    SUSHI_DECLARE_NON_COPYABLE(Vst3xWrapper)\n    /**\n     * @brief Create a new Processor that wraps the plugin found in the given path.\n     */\n    Vst3xWrapper(HostControl host_control,\n                 const std::string& vst_plugin_path,\n                 const std::string& plugin_name);\n\n    ~Vst3xWrapper() override;\n\n    /**\n     * @brief Entry point for parameter changes from the plugin editor.\n     * @param param_id Id of the parameter to change\n     * @param value New value for parameter\n     */\n    void set_parameter_change(ObjectId param_id, float value);\n\n    /* Inherited from Processor */\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    void set_channels(int inputs, int outputs) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    bool bypassed() const override;\n\n    const ParameterDescriptor* parameter_from_id(ObjectId id) const override;\n\n    std::pair<ProcessorReturnCode, float> parameter_value(ObjectId parameter_id) const override;\n\n    std::pair<ProcessorReturnCode, float> parameter_value_in_domain(ObjectId parameter_id) const override;\n\n    std::pair<ProcessorReturnCode, std::string> parameter_value_formatted(ObjectId parameter_id) const override;\n\n    std::pair<ProcessorReturnCode, std::string> property_value(ObjectId property_id) const override;\n\n    ProcessorReturnCode set_property_value(ObjectId property_id, const std::string& value) override;\n\n    bool supports_programs() const override {return _supports_programs;}\n\n    int program_count() const override {return _program_count;}\n\n    int current_program() const override;\n\n    std::string current_program_name() const override;\n\n    std::pair<ProcessorReturnCode, std::string> program_name(int program) const override;\n\n    std::pair<ProcessorReturnCode, std::vector<std::string>> all_program_names() const override;\n\n    ProcessorReturnCode set_program(int program) override;\n\n    ProcessorReturnCode set_state(ProcessorState* state, bool realtime_running) override;\n\n    ProcessorState save_state() const override;\n\n    PluginInfo info() const override;\n\n    EventId request_async_work(AsyncWorkCallback callback, void* data);\n\n    static void program_change_callback(void* arg, Event* event, int status)\n    {\n        reinterpret_cast<Vst3xWrapper*>(arg)->_program_change_callback(event, status);\n    }\n\n    static int parameter_update_callback(void* data, EventId id)\n    {\n        return reinterpret_cast<Vst3xWrapper*>(data)->_parameter_update_callback(id);\n    }\n\nprivate:\n    friend class Vst3xWrapperAccessor;\n    friend class ComponentHandler;\n\n    /**\n     * @brief Tell the plugin that we're done with it and release all resources\n     * we allocated during initialization.\n     */\n    void _cleanup();\n\n    ProcessorReturnCode _setup();\n\n    /**\n     * @brief Iterate over VsT parameters and register internal FloatParameterDescriptor\n     *        for each one of them.\n     * @return True if all parameters were registered properly.\n     */\n    bool _register_parameters();\n\n    bool _register_properties();\n\n    bool _setup_audio_buses();\n\n    bool _setup_event_buses();\n\n    bool _setup_channels();\n\n    bool _setup_processing();\n\n    bool _setup_internal_program_handling();\n\n    bool _setup_file_program_handling();\n\n    /**\n     * @brief Read output events from the plugin, convert to internal events\n     *        and forward to next plugin.\n     * @param data A ProcessData container\n     */\n    void _forward_events(Steinberg::Vst::ProcessData& data);\n\n    void _forward_params(Steinberg::Vst::ProcessData& data);\n\n    void _fill_processing_context();\n\n    inline void _add_parameter_change(Steinberg::Vst::ParamID id, float value, int sample_offset);\n\n    bool _sync_processor_to_controller();\n\n    void _program_change_callback(Event* event, int status);\n\n    int _parameter_update_callback(EventId id);\n\n    void _set_program_state(int program_id, RtState* rt_state, bool realtime_running);\n\n    void _set_bypass_state(bool bypassed, RtState* rt_state, bool realtime_running);\n\n    void _set_binary_state(std::vector<std::byte>& state);\n\n    void _set_state_rt(Vst3xRtState* state);\n\n    struct SpecialParameter\n    {\n        bool supported{false};\n        Steinberg::Vst::ParamID id{0};\n    };\n\n    struct ParameterUpdate\n    {\n        Steinberg::Vst::ParamID id;\n        float value;\n    };\n\n    struct PropertyInfo\n    {\n        bool automatable{false};\n        bool audio_thread_notification{false};\n    };\n\n    float _sample_rate;\n    bool  _supports_programs{false};\n    bool  _internal_programs{false};\n    bool  _file_based_programs{false};\n    int _main_program_list_id;\n    int _program_count{0};\n    int _current_program{0};\n\n    bool _notify_parameter_change{false};\n\n    BypassManager _bypass_manager{_bypassed};\n\n    std::vector<std::filesystem::path> _program_files;\n\n    std::string _plugin_load_name;\n    std::string _plugin_load_path;\n    ComponentHandler _component_handler;\n    SushiHostApplication _host_app;\n\n    PluginInstance _instance;\n\n    Steinberg::Vst::EventList _in_event_list{VST_WRAPPER_NOTE_EVENT_QUEUE_SIZE};\n    Steinberg::Vst::EventList _out_event_list{VST_WRAPPER_NOTE_EVENT_QUEUE_SIZE};\n    Steinberg::Vst::ParameterChanges _in_parameter_changes;\n    Steinberg::Vst::ParameterChanges _out_parameter_changes;\n\n    SushiProcessData _process_data{&_in_event_list,\n                                   &_out_event_list,\n                                   &_in_parameter_changes,\n                                   &_out_parameter_changes};\n\n    SpecialParameter _bypass_parameter;\n    SpecialParameter _program_change_parameter;\n    SpecialParameter _pitch_bend_parameter;\n    SpecialParameter _mod_wheel_parameter;\n    SpecialParameter _aftertouch_parameter;\n\n    memory_relaxed_aquire_release::CircularFifo<Vst3xRtState*, STATE_CHANGE_QUEUE_SIZE> _state_change_queue;\n    memory_relaxed_aquire_release::CircularFifo<ParameterUpdate, PARAMETER_UPDATE_QUEUE_SIZE> _parameter_update_queue;\n    std::map<Steinberg::Vst::ParamID, const ParameterDescriptor*> _parameters_by_vst3_id;\n    std::map<Steinberg::Vst::ParamID, PropertyInfo> _property_configs;\n};\n\nSteinberg::Vst::SpeakerArrangement speaker_arr_from_channels(int channels);\n\n} // end namespace sushi::internal::vst3\n\n#endif // SUSHI_VST3X_WRAPPER_H\n"
  },
  {
    "path": "src/plugins/arpeggiator_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Midi i/o plugin example with a simple arpeggiator\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <algorithm>\n\n#include \"plugins/arpeggiator_plugin.h\"\n\nnamespace sushi::internal::arpeggiator_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.arpeggiator\";\nconstexpr auto DEFAULT_LABEL = \"Arpeggiator\";\n\nconstexpr int MAX_ARP_NOTES = 8;\nconstexpr int START_NOTE = 48;\nconstexpr float MULTIPLIER_8TH_NOTE = 2.0f;\nconstexpr int   OCTAVE = 12;\n\nArpeggiatorPlugin::ArpeggiatorPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n    _range_parameter = register_int_parameter(\"range\", \"Range\", \"octaves\",\n                                              2, 1, 5,\n                                              Direction::AUTOMATABLE,\n                                              new IntParameterPreProcessor(1, 5));\n\n    assert(_range_parameter);\n    _max_input_channels = 0;\n    _max_output_channels = 0;\n    _last_note_beat = _host_control.transport()->current_beats();\n}\n\nProcessorReturnCode ArpeggiatorPlugin::init(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    return ProcessorReturnCode::OK;\n}\n\nvoid ArpeggiatorPlugin::configure(float sample_rate)\n{\n    _sample_rate = sample_rate;\n}\n\nvoid ArpeggiatorPlugin::set_bypassed(bool bypassed)\n{\n    Processor::set_bypassed(bypassed);\n}\n\nvoid ArpeggiatorPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n        case RtEventType::NOTE_ON:\n        {\n            auto typed_event = event.keyboard_event();\n            _arp.add_note(typed_event->note());\n            break;\n        }\n\n        case RtEventType::NOTE_OFF:\n        {\n            auto typed_event = event.keyboard_event();\n            _arp.remove_note(typed_event->note());\n            break;\n        }\n\n        case RtEventType::NOTE_AFTERTOUCH:\n        case RtEventType::PITCH_BEND:\n        case RtEventType::AFTERTOUCH:\n        case RtEventType::MODULATION:\n        case RtEventType::WRAPPED_MIDI_EVENT:\n            // Consume these events so they are not propagated\n            break;\n\n        case RtEventType::INT_PARAMETER_CHANGE:\n        case RtEventType::FLOAT_PARAMETER_CHANGE:\n        {\n            InternalPlugin::process_event(event);\n            auto typed_event = event.parameter_change_event();\n            if (typed_event->param_id() == _range_parameter->descriptor()->id())\n            {\n                _arp.set_range(_range_parameter->processed_value());\n            }\n            break;\n        }\n        default:\n            InternalPlugin::process_event(event);\n    }\n}\n\nvoid ArpeggiatorPlugin::process_audio(const ChunkSampleBuffer& /*in_buffer*/, ChunkSampleBuffer& /*out_buffer*/)\n{\n    if (_host_control.transport()->playing())\n    {\n        if (_host_control.transport()->current_state_change() == PlayStateChange::STARTING)\n        {\n            _last_note_beat = _host_control.transport()->current_beats();\n        }\n        double beat = _host_control.transport()->current_beats();\n        double last_beat_this_chunk = _host_control.transport()->current_beats(AUDIO_CHUNK_SIZE);\n        double beat_period = last_beat_this_chunk - beat;\n        auto notes_this_chunk = std::min(MULTIPLIER_8TH_NOTE * (last_beat_this_chunk - _last_note_beat), 2.0);\n\n        while (notes_this_chunk > 1.0)\n        {\n            double next_note_beat = _last_note_beat + 1 / MULTIPLIER_8TH_NOTE;\n            auto fraction = next_note_beat - beat - std::floor(next_note_beat - beat);\n            _last_note_beat = next_note_beat;\n            int offset = 0;\n            if (fraction > 0)\n            {\n                /* If fraction is not positive, then there was a missed beat in an underrun */\n                offset = std::min(static_cast<int>(std::round(AUDIO_CHUNK_SIZE * fraction / beat_period)),\n                                  AUDIO_CHUNK_SIZE - 1);\n            }\n\n            RtEvent note_off = RtEvent::make_note_off_event(this->id(), offset, 0, _current_note, 1.0f);\n            _current_note = _arp.next_note();\n            RtEvent note_on = RtEvent::make_note_on_event(this->id(), offset, 0, _current_note, 1.0f);\n            output_event(note_off);\n            output_event(note_on);\n\n            notes_this_chunk = MULTIPLIER_8TH_NOTE * (last_beat_this_chunk - _last_note_beat);\n        }\n    }\n\n    if (_host_control.transport()->current_state_change() == PlayStateChange::STOPPING)\n    {\n        /* Don't leave notes hanging if transport is stopped */\n        output_event(RtEvent::make_note_off_event(this->id(), 0, 0, _current_note, 1.0f));\n    }\n}\n\nstd::string_view ArpeggiatorPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\nArpeggiator::Arpeggiator()\n{\n    /* Reserving a maximum capacity and never exceeding it makes std::vector\n     * safe to use in a real-time environment */\n    _notes.reserve(MAX_ARP_NOTES);\n    _notes.push_back(START_NOTE);\n}\n\nvoid Arpeggiator::add_note(int note)\n{\n    if (_hold)\n    {\n        _hold = false;\n        _notes.clear();\n    }\n    if (_notes.size() < _notes.capacity())\n    {\n        _notes.push_back(note);\n    }\n}\n\nvoid Arpeggiator::remove_note(int note)\n{\n    if (_notes.size() <= 1)\n    {\n        _hold = true;\n        return;\n    }\n    for (auto i = _notes.begin(); i != _notes.end(); ++i)\n    {\n        if (*i == note)\n        {\n            _notes.erase(i);\n            return;\n        }\n    }\n}\n\nvoid Arpeggiator::set_range(int range)\n{\n    _range = range;\n}\n\nint Arpeggiator::next_note()\n{\n    if (++_note_idx >= static_cast<int>(_notes.size()))\n    {\n        _note_idx = 0;\n        if (++_octave_idx >= _range)\n        {\n            _octave_idx = 0;\n        }\n    }\n    return _notes[_note_idx] + _octave_idx * OCTAVE;\n}\n\n} // end namespace sushi::internal::sample_player_plugin\n\n"
  },
  {
    "path": "src/plugins/arpeggiator_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Midi i/o plugin example with a simple arpeggiator\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_ARPEGGIATOR_PLUGIN_H\n#define SUSHI_ARPEGGIATOR_PLUGIN_H\n\n#include <vector>\n\n#include \"library/internal_plugin.h\"\n\nnamespace sushi::internal::arpeggiator_plugin {\n\n/**\n * @brief Simple arpeggiator module with only 1 mode (up)\n *        The last held note will be remembered and played indefinitely\n */\nclass Arpeggiator\n{\npublic:\n    Arpeggiator();\n\n    /**\n     * @brief Add a note to the list of notes playing\n     * @param note The note number of the note to add\n     */\n    void add_note(int note);\n\n    /**\n     * @brief Remove this note from the list of notes playing\n     * @param note The note number to remove\n     */\n    void remove_note(int note);\n\n    /**\n     * @brief Set the playing range of the arpeggiator\n     * @param range The range in octaves\n     */\n    void set_range(int range);\n\n    /**\n     * @brief Call sequentially to get the full arpeggio sequence\n     * @return The note number of the next note in the sequence\n     */\n    int next_note();\n\nprivate:\n    std::vector<int> _notes;\n    int              _range{2};\n    int              _octave_idx{0};\n    int              _note_idx{-1};\n    bool             _hold{true};\n};\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nclass ArpeggiatorPlugin : public InternalPlugin, public UidHelper<ArpeggiatorPlugin>\n{\npublic:\n    explicit ArpeggiatorPlugin(HostControl host_control);\n\n    ~ArpeggiatorPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer& /*in_buffer*/, ChunkSampleBuffer& /*out_buffer*/) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    float                _sample_rate;\n    double               _last_note_beat {0};\n    int                  _current_note {0};\n\n    IntParameterValue*   _range_parameter;\n    Arpeggiator          _arp;\n};\n\n} // end namespace sushi::internal::sample_player_plugin\n\nELK_POP_WARNING\n\n#endif // SUSHI_ARPEGGIATOR_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/bitcrusher_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Bitcrusher from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"bitcrusher_plugin.h\"\n\nnamespace sushi::internal::bitcrusher_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.bitcrusher\";\nconstexpr auto DEFAULT_LABEL = \"Bitcrusher\";\n\n\nBitcrusherPlugin::BitcrusherPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _samplerate_ratio = register_float_parameter(\"sr_ratio\", \"Samplerate ratio\", \"\",\n                                                 1.0f, 0.0f, 1.0f,\n                                                 Direction::AUTOMATABLE,\n                                                 new FloatParameterPreProcessor(0.0f, 1.0f));\n    _bit_depth = register_int_parameter(\"bit_depth\", \"Bit Depth\", \"\",\n                                        16, 1, 16,\n                                        Direction::AUTOMATABLE,\n                                        new IntParameterPreProcessor(1, 16));\n\n    assert(_samplerate_ratio);\n    assert(_bit_depth);\n}\n\nProcessorReturnCode BitcrusherPlugin::init(float sample_rate)\n{\n    bw_sr_reduce_init(&_sr_reduce_coeffs);\n    bw_bd_reduce_init(&_bd_reduce_coeffs);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid BitcrusherPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_bd_reduce_reset_coeffs(&_bd_reduce_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_sr_reduce_reset_state(&_sr_reduce_coeffs, &_sr_reduce_states[i], 0.0f);\n    }\n}\n\nvoid BitcrusherPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid BitcrusherPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid BitcrusherPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_sr_reduce_set_ratio(&_sr_reduce_coeffs, _samplerate_ratio->processed_value());\n    bw_bd_reduce_set_bit_depth(&_bd_reduce_coeffs, static_cast<char>(_bit_depth->processed_value()));\n\n    if (_bypass_manager.should_process())\n    {\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            bw_sr_reduce_process(&_sr_reduce_coeffs, &_sr_reduce_states[i],\n                                 in_buffer.channel(i), out_buffer.channel(i),\n                                 AUDIO_CHUNK_SIZE);\n            bw_bd_reduce_process(&_bd_reduce_coeffs,\n                                 out_buffer.channel(i), out_buffer.channel(i),\n                                 AUDIO_CHUNK_SIZE);\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view BitcrusherPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::bitcrusher_plugin\n\n"
  },
  {
    "path": "src/plugins/brickworks/bitcrusher_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Bitcrusher from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef BITCRUSHER_PLUGIN_H\n#define BITCRUSHER_PLUGIN_H\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_CONVERSION_FROM_INT_TO_FLOAT\nELK_DISABLE_CONVERSION_FROM_SIZE_T_TO_INT\n#include <bw_sr_reduce.h>\n#include <bw_bd_reduce.h>\nELK_POP_WARNING\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::bitcrusher_plugin {\n\nclass Accessor;\n\nclass BitcrusherPlugin : public InternalPlugin, public UidHelper<BitcrusherPlugin>\n{\npublic:\n    explicit BitcrusherPlugin(HostControl hostControl);\n\n    ~BitcrusherPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override\n    {\n        _sample_rate = sample_rate;\n    }\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    friend Accessor;\n\n    BypassManager _bypass_manager;\n    float _sample_rate{0};\n\n    FloatParameterValue* _samplerate_ratio;\n    IntParameterValue* _bit_depth;\n\n    bw_sr_reduce_coeffs _sr_reduce_coeffs;\n    bw_bd_reduce_coeffs _bd_reduce_coeffs;\n    std::array<bw_sr_reduce_state, MAX_TRACK_CHANNELS> _sr_reduce_states;\n};\n\n} // namespace sushi::internal::bitcrusher_plugin\n\nELK_POP_WARNING\n\n#endif // BITCRUSHER_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/cab_sim_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Cabinet simulator from Brickworks library\n * @copyright 2017-2025, Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"cab_sim_plugin.h\"\n\nnamespace sushi::internal::cab_sim_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.cab_sim\";\nconstexpr auto DEFAULT_LABEL = \"Cab Simulator\";\n\n\nCabSimPlugin::CabSimPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _cutoff_low = register_float_parameter(\"cutoff_low\", \"Cutoff Low\", \"\",\n                                           0.5f, 0.0f, 1.0f,\n                                           Direction::AUTOMATABLE,\n                                           new FloatParameterPreProcessor(0.0f, 1.0f));\n    _cutoff_high = register_float_parameter(\"cutoff_high\", \"Cutoff High\", \"\",\n                                            0.5f, 0.0f, 1.0f,\n                                            Direction::AUTOMATABLE,\n                                            new FloatParameterPreProcessor(0.0f, 1.0f));\n    _tone = register_float_parameter(\"tone\", \"Tone\", \"\",\n                                     0.5f, 0.0f, 1.0f,\n                                     Direction::AUTOMATABLE,\n                                     new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    assert(_cutoff_low);\n    assert(_cutoff_high);\n    assert(_tone);\n}\n\nProcessorReturnCode CabSimPlugin::init(float sample_rate)\n{\n    bw_cab_init(&_cab_coeffs);\n    bw_cab_set_sample_rate(&_cab_coeffs, sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid CabSimPlugin::configure(float sample_rate)\n{\n    bw_cab_set_sample_rate(&_cab_coeffs, sample_rate);\n    return;\n}\n\nvoid CabSimPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_cab_reset_coeffs(&_cab_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_cab_reset_state(&_cab_coeffs, &_cab_state[i], 0.0f);\n    }\n}\n\nvoid CabSimPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid CabSimPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid CabSimPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_cab_set_cutoff_low(&_cab_coeffs, _cutoff_low->processed_value());\n    bw_cab_set_cutoff_high(&_cab_coeffs, _cutoff_high->processed_value());\n    bw_cab_set_tone(&_cab_coeffs, _tone->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_cab_update_coeffs_ctrl(&_cab_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_cab_update_coeffs_audio(&_cab_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                *out_channel_ptrs[i]++ = bw_cab_process1(&_cab_coeffs, &_cab_state[i],\n                                                         *in_channel_ptrs[i]++);\n            }\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view CabSimPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::cab_sim_plugin\n"
  },
  {
    "path": "src/plugins/brickworks/cab_sim_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Cabinet simulator from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef CAB_SIM_PLUGIN_H\n#define CAB_SIM_PLUGIN_H\n\n#include <bw_cab.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::cab_sim_plugin {\n\nclass CabSimPlugin : public InternalPlugin, public UidHelper<CabSimPlugin>\n{\npublic:\n    explicit CabSimPlugin(HostControl hostControl);\n\n    ~CabSimPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate{0};\n\n    FloatParameterValue* _cutoff_low;\n    FloatParameterValue* _cutoff_high;\n    FloatParameterValue* _tone;\n\n    bw_cab_coeffs _cab_coeffs;\n    std::array<bw_cab_state, MAX_TRACK_CHANNELS> _cab_state;\n};\n\n} // namespace sushi::internal::cab_sim_plugin\n\nELK_POP_WARNING\n\n#endif // CAB_SIM_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/chorus_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Chorus from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"chorus_plugin.h\"\n\nnamespace sushi::internal::chorus_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.chorus\";\nconstexpr auto DEFAULT_LABEL = \"Chorus\";\n\nconstexpr float CHORUS_AMOUNT_SCALE = 0.004f;\n\n\nChorusPlugin::ChorusPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    // The low-level module bw_chorus.h exposes other parameters\n    // (delay & three coefficients for the direct / modulation / feedback branches)\n\n    // but the high-level chorus example pre-configures them accordingly to Dattoro's reccomendations\n    _rate = register_float_parameter(\"rate\", \"Rate\", \"Hz\",\n                                      1.0f, 0.01f, 2.0f,\n                                      Direction::AUTOMATABLE,\n                                      new CubicWarpPreProcessor(0.01f, 2.0f));\n    _amount = register_float_parameter(\"amount\", \"Amount\", \"\",\n                                       0.0f, 0.0f, 1.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    assert(_rate);\n    assert(_amount);\n}\n\nProcessorReturnCode ChorusPlugin::init(float sample_rate)\n{\n    // Default values taken from Brickworks example fx_chorus\n    bw_chorus_init(&_chorus_coeffs, 0.01f);\n    bw_chorus_set_delay(&_chorus_coeffs, 0.005f);\n    bw_chorus_set_coeff_x(&_chorus_coeffs, 0.7071f);\n    bw_chorus_set_coeff_mod(&_chorus_coeffs, 1.f);\n    bw_chorus_set_coeff_fb(&_chorus_coeffs, -0.7071f);\n    configure(sample_rate);\n\n    return ProcessorReturnCode::OK;\n}\n\nvoid ChorusPlugin::configure(float sample_rate)\n{\n    bw_chorus_set_sample_rate(&_chorus_coeffs, sample_rate);\n    return;\n}\n\nvoid ChorusPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_chorus_reset_coeffs(&_chorus_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        _delay_mem_areas[i].resize(bw_chorus_mem_req(&_chorus_coeffs));\n        bw_chorus_mem_set(&_chorus_coeffs, &_chorus_states[i], _delay_mem_areas[i].data());\n    }\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_chorus_reset_state(&_chorus_coeffs, &_chorus_states[i], 0.0f);\n    }\n}\n\nvoid ChorusPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid ChorusPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid ChorusPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_chorus_set_rate(&_chorus_coeffs, _rate->processed_value());\n    bw_chorus_set_amount(&_chorus_coeffs, _amount->processed_value() * CHORUS_AMOUNT_SCALE);\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_chorus_update_coeffs_ctrl(&_chorus_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_chorus_update_coeffs_audio(&_chorus_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                *out_channel_ptrs[i]++ = bw_chorus_process1(&_chorus_coeffs, &_chorus_states[i],\n                                                            *in_channel_ptrs[i]++);\n            }\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view ChorusPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::chorus_plugin\n\n"
  },
  {
    "path": "src/plugins/brickworks/chorus_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Chorus from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef CHORUS_PLUGIN_H\n#define CHORUS_PLUGIN_H\n\n#include <vector>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_SHORTEN_64_TO_32\nELK_DISABLE_CONVERSION_FROM_SIZE_T_TO_INT\n#include <bw_chorus.h>\nELK_POP_WARNING\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::chorus_plugin {\n\nclass ChorusPlugin : public InternalPlugin, public UidHelper<ChorusPlugin>\n{\npublic:\n    explicit ChorusPlugin(HostControl hostControl);\n\n    ~ChorusPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager{false, std::chrono::milliseconds(100)};\n    float _sample_rate{0};\n\n    FloatParameterValue* _rate;\n    FloatParameterValue* _amount;\n\n    bw_chorus_coeffs _chorus_coeffs;\n    std::array<bw_chorus_state, MAX_TRACK_CHANNELS> _chorus_states;\n    std::array<std::vector<std::byte>, MAX_TRACK_CHANNELS> _delay_mem_areas;\n};\n\n} // namespace sushi::internal::chorus_plugin\n\nELK_POP_WARNING\n\n#endif // CHORUS_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/clip_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Clip from Brickworks library, with internal 2x resampling\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"clip_plugin.h\"\n\nnamespace sushi::internal::clip_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.clip\";\nconstexpr auto DEFAULT_LABEL = \"Clip\";\n\n\nClipPlugin::ClipPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _bias = register_float_parameter(\"bias\", \"Bias\", \"\",\n                                     0.0f, -2.5f, 2.5f,\n                                     Direction::AUTOMATABLE,\n                                     new FloatParameterPreProcessor(-2.5f, 2.5f));\n    _gain = register_float_parameter(\"gain\", \"Gain\", \"\",\n                                     1.0f, 0.1f, 10.0f,\n                                     Direction::AUTOMATABLE,\n                                     new CubicWarpPreProcessor(0.1f, 10.0f));\n\n    assert(_bias);\n    assert(_gain);\n}\n\nProcessorReturnCode ClipPlugin::init(float sample_rate)\n{\n    bw_clip_init(&_clip_coeffs);\n    bw_src_int_init(&_src_up_coeffs, 2);\n    bw_src_int_init(&_src_down_coeffs, -2);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid ClipPlugin::configure(float sample_rate)\n{\n    bw_clip_set_sample_rate(&_clip_coeffs, 2.0f * sample_rate);\n}\n\nvoid ClipPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_clip_reset_coeffs(&_clip_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_clip_reset_state(&_clip_coeffs, &_clip_states[i], 0.0f);\n        bw_src_int_reset_state(&_src_up_coeffs, &_src_up_states[i], 0.0f);\n        bw_src_int_reset_state(&_src_down_coeffs, &_src_down_states[i], 0.0f);\n    }\n}\n\nvoid ClipPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid ClipPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid ClipPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_clip_set_bias(&_clip_coeffs, _bias->processed_value());\n    bw_clip_set_gain(&_clip_coeffs, _gain->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        bw_clip_update_coeffs_ctrl(&_clip_coeffs);\n        int n = 0;\n        while (n < AUDIO_CHUNK_SIZE)\n        {\n            // 2x upsample\n            int frames_left = std::min(AUDIO_CHUNK_SIZE - n, AUDIO_CHUNK_SIZE >> 1);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                bw_src_int_process(&_src_up_coeffs, &_src_up_states[i],\n                                   in_buffer.channel(i) + n, _tmp_buf.channel(i), frames_left);\n            }\n            // upsampled clip with coefficient interp.\n            int frames_upsample = frames_left << 1;\n            for (int n_up = 0; n_up < frames_upsample; n_up++)\n            {\n                bw_clip_update_coeffs_audio(&_clip_coeffs);\n                for (int i = 0; i < _current_input_channels; i++)\n                {\n                    float* buf = _tmp_buf.channel(i);\n                    // we default to the version without gain compensation\n                    buf[n_up] = bw_clip_process1(&_clip_coeffs, &_clip_states[i], buf[n_up]);\n                }\n            }\n            // 2x downsample\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                bw_src_int_process(&_src_down_coeffs, &_src_down_states[i],\n                                   _tmp_buf.channel(i), out_buffer.channel(i) + n, frames_upsample);\n            }\n            n += frames_left;\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view ClipPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::clip_plugin\n\n"
  },
  {
    "path": "src/plugins/brickworks/clip_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Clip from Brickworks library, with internal 2x resampling\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef CLIP_PLUGIN_H\n#define CLIP_PLUGIN_H\n\n#include <bw_clip.h>\n#include <bw_src_int.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::clip_plugin {\n\nclass ClipPlugin : public InternalPlugin, public UidHelper<ClipPlugin>\n{\npublic:\n    explicit ClipPlugin(HostControl hostControl);\n\n    ~ClipPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate{0};\n\n    FloatParameterValue* _bias;\n    FloatParameterValue* _gain;\n\n    bw_clip_coeffs _clip_coeffs;\n    bw_src_int_coeffs _src_up_coeffs;\n    bw_src_int_coeffs _src_down_coeffs;\n    std::array<bw_clip_state, MAX_TRACK_CHANNELS>   _clip_states;\n    std::array<bw_src_int_state, MAX_TRACK_CHANNELS> _src_up_states;\n    std::array<bw_src_int_state, MAX_TRACK_CHANNELS> _src_down_states;\n\n    ChunkSampleBuffer _tmp_buf{MAX_TRACK_CHANNELS};\n};\n\n} // namespace sushi::internal::clip_plugin\n\nELK_POP_WARNING\n\n#endif // CLIP_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/combdelay_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Comb delay from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"combdelay_plugin.h\"\n\nnamespace sushi::internal::comb_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.comb_delay\";\nconstexpr auto DEFAULT_LABEL = \"Comb Delay\";\n\n\nCombPlugin::CombPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _ff_delay = register_float_parameter(\"ff_delay\", \"Feed-forward Delay\", \"sec\",\n                                         0.05f, 0.0f, 1.0f,\n                                         Direction::AUTOMATABLE,\n                                         new FloatParameterPreProcessor(0.0f, 1.0f));\n    _fb_delay = register_float_parameter(\"fb_delay\", \"Feedback Delay\", \"sec\",\n                                         0.05f, 0.0f, 1.0f,\n                                         Direction::AUTOMATABLE,\n                                         new FloatParameterPreProcessor(0.0f, 1.0f));\n    _blend = register_float_parameter(\"blend\", \"Blend\", \"\",\n                                      1.0f, 0.0f, 1.0f,\n                                      Direction::AUTOMATABLE,\n                                      new FloatParameterPreProcessor(0.0f, 1.0f));\n    _ff_coeff = register_float_parameter(\"ff_coeff\", \"Feed-forward Coefficient\", \"\",\n                                         0.0f, -1.0f, 1.0f,\n                                         Direction::AUTOMATABLE,\n                                         new FloatParameterPreProcessor(-1.0f, 1.0f));\n    _fb_coeff = register_float_parameter(\"fb_coeff\", \"Feedback Coefficient\", \"\",\n                                         0.0f, -0.995f, 0.995f,\n                                         Direction::AUTOMATABLE,\n                                         new FloatParameterPreProcessor(-0.995f, 0.995f));\n\n    assert(_ff_delay);\n    assert(_fb_delay);\n    assert(_blend);\n    assert(_ff_coeff);\n    assert(_fb_coeff);\n}\n\nProcessorReturnCode CombPlugin::init(float sample_rate)\n{\n    // Default values taken from Brickworks example fx_comb\n    bw_comb_init(&_comb_coeffs, 1.0f);\n    configure(sample_rate);\n\n    return ProcessorReturnCode::OK;\n}\n\nvoid CombPlugin::configure(float sample_rate)\n{\n    bw_comb_set_sample_rate(&_comb_coeffs, sample_rate);\n    return;\n}\n\nvoid CombPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_comb_reset_coeffs(&_comb_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        _delay_mem_areas[i].resize(bw_comb_mem_req(&_comb_coeffs));\n        bw_comb_mem_set(&_comb_coeffs, &_comb_states[i], _delay_mem_areas[i].data());\n    }\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_comb_reset_state(&_comb_coeffs, &_comb_states[i], 0.0f);\n    }\n}\n\nvoid CombPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid CombPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid CombPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_comb_set_delay_ff(&_comb_coeffs, _ff_delay->processed_value());\n    bw_comb_set_delay_fb(&_comb_coeffs, _fb_delay->processed_value());\n    bw_comb_set_coeff_blend(&_comb_coeffs, _blend->processed_value());\n    bw_comb_set_coeff_ff(&_comb_coeffs, _ff_coeff->processed_value());\n    bw_comb_set_coeff_fb(&_comb_coeffs, _fb_coeff->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_comb_update_coeffs_ctrl(&_comb_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_comb_update_coeffs_audio(&_comb_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                *out_channel_ptrs[i]++ = bw_comb_process1(&_comb_coeffs, &_comb_states[i],\n                                                            *in_channel_ptrs[i]++);\n            }\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view CombPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::comb_plugin\n\n"
  },
  {
    "path": "src/plugins/brickworks/combdelay_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Comb delay from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <vector>\n\n#ifndef COMBDELAY_PLUGIN_H\n#define COMBDELAY_PLUGIN_H\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_SHORTEN_64_TO_32\nELK_DISABLE_CONVERSION_FROM_SIZE_T_TO_INT\n#include <bw_comb.h>\nELK_POP_WARNING\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::comb_plugin {\n\nclass CombPlugin : public InternalPlugin, public UidHelper<CombPlugin>\n{\npublic:\n    explicit CombPlugin(HostControl hostControl);\n\n    ~CombPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager{false, std::chrono::milliseconds(100)};\n    float _sample_rate{0};\n\n    FloatParameterValue* _ff_delay;\n    FloatParameterValue* _fb_delay;\n    FloatParameterValue* _blend;\n    FloatParameterValue* _ff_coeff;\n    FloatParameterValue* _fb_coeff;\n\n    bw_comb_coeffs _comb_coeffs;\n    std::array<bw_comb_state, MAX_TRACK_CHANNELS> _comb_states;\n    std::array<std::vector<std::byte>, MAX_TRACK_CHANNELS> _delay_mem_areas;\n};\n\n} // namespace sushi::internal::comb_plugin\n\nELK_POP_WARNING\n\n#endif // COMBDELAY_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/compressor_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Dynamics compressor from Brickworks library\n * @copyright 2017-2025, Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"compressor_plugin.h\"\n\nnamespace sushi::internal::compressor_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.compressor\";\nconstexpr auto DEFAULT_LABEL = \"Compressor\";\n\nconstexpr float MINUS_3DB = 0.7071067811865476f;\n\n\nCompressorPlugin::CompressorPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _threshold = register_float_parameter(\"threshold\", \"Threshold\", \"dB\",\n                                          0.0f, -60.0f, 12.0f,\n                                          Direction::AUTOMATABLE,\n                                          new FloatParameterPreProcessor(-60.0f, 12.0f));\n    _ratio = register_float_parameter(\"ratio\", \"Ratio\", \"\",\n                                      1.0f, 0.0f, 1.0f,\n                                      Direction::AUTOMATABLE,\n                                      new FloatParameterPreProcessor(0.0f, 1.0f));\n    _attack = register_float_parameter(\"attack\", \"Attack\", \"s\",\n                                       0.0f, 0.0f, 1.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(0.0f, 1.0f));\n    _release = register_float_parameter(\"release\", \"Release\", \"s\",\n                                      0.0f, 0.0f, 1.0f,\n                                      Direction::AUTOMATABLE,\n                                      new FloatParameterPreProcessor(0.0f, 1.0f));\n    _gain = register_float_parameter(\"gain\", \"Gain\", \"dB\",\n                                     0.0f, -60.0f, 60.0f,\n                                     Direction::AUTOMATABLE,\n                                     new FloatParameterPreProcessor(-60.0f, 60.0f));\n\n    assert(_threshold);\n    assert(_ratio);\n    assert(_attack);\n    assert(_release);\n    assert(_gain);\n}\n\nProcessorReturnCode CompressorPlugin::init(float sample_rate)\n{\n    bw_comp_init(&_compressor_coeffs);\n    bw_comp_set_sample_rate(&_compressor_coeffs, sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid CompressorPlugin::configure(float sample_rate)\n{\n    bw_comp_set_sample_rate(&_compressor_coeffs, sample_rate);\n    return;\n}\n\nvoid CompressorPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_comp_reset_coeffs(&_compressor_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_comp_reset_state(&_compressor_coeffs, &_compressor_state[i], 0.0f, 0.0f);\n    }\n}\n\nvoid CompressorPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid CompressorPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid CompressorPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_comp_set_thresh_dBFS(&_compressor_coeffs, _threshold->processed_value());\n    bw_comp_set_ratio(&_compressor_coeffs, _ratio->processed_value());\n    bw_comp_set_attack_tau(&_compressor_coeffs, _attack->processed_value());\n    bw_comp_set_release_tau(&_compressor_coeffs, _release->processed_value());\n    bw_comp_set_gain_dB(&_compressor_coeffs, _gain->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_comp_update_coeffs_ctrl(&_compressor_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            std::array<float, MAX_TRACK_CHANNELS> input_samples {};\n\n            bw_comp_update_coeffs_audio(&_compressor_coeffs);\n            float control_sig = 0.0f;\n\n            for (int i = 0; i < _current_input_channels; ++i)\n            {\n                input_samples[i] = *in_channel_ptrs[i]++;\n                control_sig += input_samples[i] * MINUS_3DB;\n            }\n\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                *out_channel_ptrs[i]++ = bw_comp_process1(&_compressor_coeffs, &_compressor_state[i],\n                                                          input_samples[i], control_sig);\n            }\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view CompressorPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::compressor_plugin\n\n"
  },
  {
    "path": "src/plugins/brickworks/compressor_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Dynamics compressor from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef COMPRESSOR_PLUGIN_H\n#define COMPRESSOR_PLUGIN_H\n\n#include <bw_comp.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::compressor_plugin {\n\nclass CompressorPlugin : public InternalPlugin, public UidHelper<CompressorPlugin>\n{\npublic:\n    explicit CompressorPlugin(HostControl hostControl);\n\n    ~CompressorPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate{0};\n\n    FloatParameterValue* _threshold;\n    FloatParameterValue* _ratio;\n    FloatParameterValue* _attack;\n    FloatParameterValue* _release;\n    FloatParameterValue* _gain;\n\n    bw_comp_coeffs _compressor_coeffs;\n    std::array<bw_comp_state, MAX_TRACK_CHANNELS> _compressor_state;\n};\n\n} // namespace sushi::internal::compressor_plugin\n\nELK_POP_WARNING\n\n#endif // COMPRESSOR_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/dist_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Distortion from Brickworks library, with internal 2x resampling\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"dist_plugin.h\"\n\nnamespace sushi::internal::dist_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.dist\";\nconstexpr auto DEFAULT_LABEL = \"Distortion\";\n\n\nDistPlugin::DistPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _dist = register_float_parameter(\"dist\", \"Dist\", \"\",\n                                     0.0f, 0.0f, 1.0f,\n                                     Direction::AUTOMATABLE,\n                                     new FloatParameterPreProcessor(0.0f, 1.0f));\n    _tone = register_float_parameter(\"tone\", \"Tone\", \"\",\n                                     0.5f, 0.0f, 1.0f,\n                                     Direction::AUTOMATABLE,\n                                     new FloatParameterPreProcessor(0.0f, 1.0f));\n    _volume = register_float_parameter(\"gain\", \"Gain\", \"\",\n                                       1.0f, 0.0f, 1.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    assert(_dist);\n    assert(_tone);\n    assert(_volume);\n}\n\nProcessorReturnCode DistPlugin::init(float sample_rate)\n{\n    bw_dist_init(&_dist_coeffs);\n    bw_src_int_init(&_src_up_coeffs, 2);\n    bw_src_int_init(&_src_down_coeffs, -2);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid DistPlugin::configure(float sample_rate)\n{\n    bw_dist_set_sample_rate(&_dist_coeffs, 2.0f * sample_rate);\n}\n\nvoid DistPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_dist_reset_coeffs(&_dist_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_dist_reset_state(&_dist_coeffs, &_dist_states[i], 0.0f);\n        bw_src_int_reset_state(&_src_up_coeffs, &_src_up_states[i], 0.0f);\n        bw_src_int_reset_state(&_src_down_coeffs, &_src_down_states[i], 0.0f);\n    }\n}\n\nvoid DistPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid DistPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid DistPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_dist_set_distortion(&_dist_coeffs, _dist->processed_value());\n    bw_dist_set_tone(&_dist_coeffs, _tone->processed_value());\n    bw_dist_set_volume(&_dist_coeffs, _volume->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        bw_dist_update_coeffs_ctrl(&_dist_coeffs);\n        int n = 0;\n        while (n < AUDIO_CHUNK_SIZE)\n        {\n            // 2x upsample\n            int frames_left = std::min(AUDIO_CHUNK_SIZE - n, AUDIO_CHUNK_SIZE >> 1);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                bw_src_int_process(&_src_up_coeffs, &_src_up_states[i],\n                                   in_buffer.channel(i) + n, _tmp_buf.channel(i), frames_left);\n            }\n            // upsampled dist with coefficient interp.\n            int frames_upsample = frames_left << 1;\n            for (int n_up = 0; n_up < frames_upsample; n_up++)\n            {\n                bw_dist_update_coeffs_audio(&_dist_coeffs);\n                for (int i = 0; i < _current_input_channels; i++)\n                {\n                    float* buf = _tmp_buf.channel(i);\n                    buf[n_up] = bw_dist_process1(&_dist_coeffs, &_dist_states[i], buf[n_up]);\n                }\n            }\n            // 2x downsample\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                bw_src_int_process(&_src_down_coeffs, &_src_down_states[i],\n                                   _tmp_buf.channel(i), out_buffer.channel(i) + n, frames_upsample);\n            }\n            n += frames_left;\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view DistPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::dist_plugin\n\n\n"
  },
  {
    "path": "src/plugins/brickworks/dist_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Distortion from Brickworks library, with internal 2x resampling\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef DIST_PLUGIN_H\n#define DIST_PLUGIN_H\n\n#include <bw_dist.h>\n#include <bw_src_int.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::dist_plugin {\n\nclass DistPlugin : public InternalPlugin, public UidHelper<DistPlugin>\n{\npublic:\n    explicit DistPlugin(HostControl hostControl);\n\n    ~DistPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate{0};\n\n    FloatParameterValue* _dist;\n    FloatParameterValue* _tone;\n    FloatParameterValue* _volume;\n\n    bw_dist_coeffs _dist_coeffs;\n    bw_src_int_coeffs _src_up_coeffs;\n    bw_src_int_coeffs _src_down_coeffs;\n    std::array<bw_dist_state, MAX_TRACK_CHANNELS> _dist_states;\n    std::array<bw_src_int_state, MAX_TRACK_CHANNELS> _src_up_states;\n    std::array<bw_src_int_state, MAX_TRACK_CHANNELS> _src_down_states;\n\n    ChunkSampleBuffer _tmp_buf{MAX_TRACK_CHANNELS};\n};\n\n} // namespace sushi::internal::dist_plugin\n\nELK_POP_WARNING\n\n#endif // DIST_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/drive_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Drive from Brickworks library, with internal 2x resampling\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"drive_plugin.h\"\n\nnamespace sushi::internal::drive_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.drive\";\nconstexpr auto DEFAULT_LABEL = \"Drive\";\n\n\nDrivePlugin::DrivePlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _drive = register_float_parameter(\"drive\", \"Drive\", \"\",\n                                     0.0f, 0.0f, 1.0f,\n                                     Direction::AUTOMATABLE,\n                                     new FloatParameterPreProcessor(0.0f, 1.0f));\n    _tone = register_float_parameter(\"tone\", \"Tone\", \"\",\n                                     0.5f, 0.0f, 1.0f,\n                                     Direction::AUTOMATABLE,\n                                     new FloatParameterPreProcessor(0.0f, 1.0f));\n    _volume = register_float_parameter(\"gain\", \"Gain\", \"\",\n                                       1.0f, 0.0f, 1.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    assert(_drive);\n    assert(_tone);\n    assert(_volume);\n}\n\nProcessorReturnCode DrivePlugin::init(float sample_rate)\n{\n    bw_drive_init(&_drive_coeffs);\n    bw_src_int_init(&_src_up_coeffs, 2);\n    bw_src_int_init(&_src_down_coeffs, -2);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid DrivePlugin::configure(float sample_rate)\n{\n    bw_drive_set_sample_rate(&_drive_coeffs, 2.0f * sample_rate);\n    return;\n}\n\nvoid DrivePlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_drive_reset_coeffs(&_drive_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_drive_reset_state(&_drive_coeffs, &_drive_states[i], 0.0f);\n        bw_src_int_reset_state(&_src_up_coeffs, &_src_up_states[i], 0.0f);\n        bw_src_int_reset_state(&_src_down_coeffs, &_src_down_states[i], 0.0f);\n    }\n}\n\nvoid DrivePlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid DrivePlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\n\nvoid DrivePlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_drive_set_drive(&_drive_coeffs, _drive->processed_value());\n    bw_drive_set_tone(&_drive_coeffs, _tone->processed_value());\n    bw_drive_set_volume(&_drive_coeffs, _volume->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        bw_drive_update_coeffs_ctrl(&_drive_coeffs);\n        int n = 0;\n        while (n < AUDIO_CHUNK_SIZE)\n        {\n            // 2x upsample\n            int frames_left = std::min(AUDIO_CHUNK_SIZE - n, AUDIO_CHUNK_SIZE >> 1);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                bw_src_int_process(&_src_up_coeffs, &_src_up_states[i],\n                                   in_buffer.channel(i) + n, _tmp_buf.channel(i), frames_left);\n            }\n            // upsampled drive with coefficient interp.\n            int frames_upsample = frames_left << 1;\n            for (int n_up = 0; n_up < frames_upsample; n_up++)\n            {\n                bw_drive_update_coeffs_audio(&_drive_coeffs);\n                for (int i = 0; i < _current_input_channels; i++)\n                {\n                    float* buf = _tmp_buf.channel(i);\n                    buf[n_up] = bw_drive_process1(&_drive_coeffs, &_drive_states[i], buf[n_up]);\n                }\n            }\n            // 2x downsample\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                bw_src_int_process(&_src_down_coeffs, &_src_down_states[i],\n                                   _tmp_buf.channel(i), out_buffer.channel(i) + n, frames_upsample);\n            }\n            n += frames_left;\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view DrivePlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::drive_plugin\n\n\n"
  },
  {
    "path": "src/plugins/brickworks/drive_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Drive from Brickworks library, with internal 2x resampling\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef DRIVE_PLUGIN_H\n#define DRIVE_PLUGIN_H\n\n#include <bw_drive.h>\n#include <bw_src_int.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::drive_plugin {\n\nclass DrivePlugin : public InternalPlugin, public UidHelper<DrivePlugin>\n{\npublic:\n    explicit DrivePlugin(HostControl hostControl);\n\n    ~DrivePlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate{0};\n\n    FloatParameterValue* _drive;\n    FloatParameterValue* _tone;\n    FloatParameterValue* _volume;\n\n    bw_drive_coeffs _drive_coeffs;\n    bw_src_int_coeffs _src_up_coeffs;\n    bw_src_int_coeffs _src_down_coeffs;\n    std::array<bw_drive_state, MAX_TRACK_CHANNELS> _drive_states;\n    std::array<bw_src_int_state, MAX_TRACK_CHANNELS> _src_up_states;\n    std::array<bw_src_int_state, MAX_TRACK_CHANNELS> _src_down_states;\n\n    ChunkSampleBuffer _tmp_buf{MAX_TRACK_CHANNELS};\n};\n\n} // namespace sushi::internal::drive_plugin\n\nELK_POP_WARNING\n\n#endif // DRIVE_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/eq3band_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief 3-band equalizer from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"eq3band_plugin.h\"\n\nnamespace sushi::internal::eq3band_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.eq3band\";\nconstexpr auto DEFAULT_LABEL = \"3-band Equalizer\";\n\n\nEq3bandPlugin::Eq3bandPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _lowshelf_freq = register_float_parameter(\"lowshelf_freq\", \"Low-shelf Frequency\", \"Hz\",\n                                              125.0f, 25.0f, 1'000.0f,\n                                              Direction::AUTOMATABLE,\n                                              new CubicWarpPreProcessor(25.0f, 1'000.0f));\n    _lowshelf_gain = register_float_parameter(\"lowshelf_gain\", \"Low-shelf Gain\", \"dB\",\n                                              0.0f, -24.0f, 24.0f,\n                                              Direction::AUTOMATABLE,\n                                              new dBToLinPreProcessor(-24.0f, 24.0f));\n    _lowshelf_q = register_float_parameter(\"lowshelf_q\", \"Low-shelf Q\", \"\",\n                                           1.0f, 0.5f, 5.0f,\n                                           Direction::AUTOMATABLE,\n                                           new FloatParameterPreProcessor(0.5f, 5.0f));\n\n    _peak_freq = register_float_parameter(\"peak_freq\", \"Peak frequency\", \"Hz\",\n                                          1'000.0f, 25.0f, 20'000.0f,\n                                          Direction::AUTOMATABLE,\n                                          new CubicWarpPreProcessor(25.0f, 20'000.0f));\n    _peak_gain = register_float_parameter(\"peak_gain\", \"Peak Gain\", \"dB\",\n                                          0.0f, -24.0f, 24.0f,\n                                          Direction::AUTOMATABLE,\n                                          new dBToLinPreProcessor(-24.0f, 24.0f));\n    _peak_q = register_float_parameter(\"peak_q\", \"Peak Q\", \"\",\n                                       1.0f, 0.5f, 5.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(0.5f, 5.0f));\n\n    _highshelf_freq = register_float_parameter(\"highshelf_freq\", \"High-shelf frequency\", \"Hz\",\n                                               4'000.0f, 1'000.0f, 20'000.0f,\n                                               Direction::AUTOMATABLE,\n                                               new CubicWarpPreProcessor(1'000.0f, 20'000.0f));\n    _highshelf_gain = register_float_parameter(\"highshelf_gain\", \"High-shelf Gain\", \"dB\",\n                                              0.0f, -24.0f, 24.0f,\n                                              Direction::AUTOMATABLE,\n                                              new dBToLinPreProcessor(-24.0f, 24.0f));\n    _highshelf_q = register_float_parameter(\"highshelf_q\", \"High-shelf Q\", \"\",\n                                           1.0f, 0.5f, 5.0f,\n                                           Direction::AUTOMATABLE,\n                                           new FloatParameterPreProcessor(0.5f, 5.0f));\n\n    assert(_lowshelf_freq);\n    assert(_lowshelf_gain);\n    assert(_lowshelf_q);\n    assert(_peak_freq);\n    assert(_peak_gain);\n    assert(_peak_q);\n    assert(_highshelf_freq);\n    assert(_highshelf_gain);\n    assert(_highshelf_q);\n}\n\nProcessorReturnCode Eq3bandPlugin::init(float sample_rate)\n{\n    bw_ls2_init(&_ls2_coeffs);\n    bw_peak_init(&_peak_coeffs);\n    bw_hs2_init(&_hs2_coeffs);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid Eq3bandPlugin::configure(float sample_rate)\n{\n    bw_ls2_set_sample_rate(&_ls2_coeffs, sample_rate);\n    bw_peak_set_sample_rate(&_peak_coeffs, sample_rate);\n    bw_hs2_set_sample_rate(&_hs2_coeffs, sample_rate);\n    return;\n}\n\nvoid Eq3bandPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_ls2_reset_coeffs(&_ls2_coeffs);\n    bw_peak_reset_coeffs(&_peak_coeffs);\n    bw_hs2_reset_coeffs(&_hs2_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_ls2_reset_state(&_ls2_coeffs, &_ls2_states[i], 0.0f);\n        bw_peak_reset_state(&_peak_coeffs, &_peak_states[i], 0.0f);\n        bw_hs2_reset_state(&_hs2_coeffs, &_hs2_states[i], 0.0f);\n    }\n}\n\nvoid Eq3bandPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid Eq3bandPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid Eq3bandPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_ls2_set_cutoff(&_ls2_coeffs, _lowshelf_freq->processed_value());\n    bw_ls2_set_dc_gain_lin(&_ls2_coeffs, _lowshelf_gain->processed_value());\n    bw_ls2_set_Q(&_ls2_coeffs, _lowshelf_q->processed_value());\n\n    bw_peak_set_cutoff(&_peak_coeffs, _peak_freq->processed_value());\n    bw_peak_set_peak_gain_lin(&_peak_coeffs, _peak_gain->processed_value());\n    bw_peak_set_bandwidth(&_peak_coeffs, _peak_q->processed_value());\n\n    bw_hs2_set_cutoff(&_hs2_coeffs, _highshelf_freq->processed_value());\n    bw_hs2_set_high_gain_lin(&_hs2_coeffs, _highshelf_gain->processed_value());\n    bw_hs2_set_Q(&_hs2_coeffs, _highshelf_q->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_ls2_update_coeffs_ctrl(&_ls2_coeffs);\n        bw_peak_update_coeffs_ctrl(&_peak_coeffs);\n        bw_hs2_update_coeffs_ctrl(&_hs2_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_ls2_update_coeffs_audio(&_ls2_coeffs);\n            bw_peak_update_coeffs_audio(&_peak_coeffs);\n            bw_hs2_update_coeffs_audio(&_hs2_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                float x = bw_ls2_process1(&_ls2_coeffs, &_ls2_states[i],\n                                          *in_channel_ptrs[i]++);\n                float y = bw_peak_process1(&_peak_coeffs, &_peak_states[i], x);\n                *out_channel_ptrs[i]++ = bw_hs2_process1(&_hs2_coeffs, &_hs2_states[i], y);\n            }\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view Eq3bandPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::eq3band_plugin\n\n"
  },
  {
    "path": "src/plugins/brickworks/eq3band_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief 3-band equalizer from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef EQ3BAND_PLUGIN_H\n#define EQ3BAND_PLUGIN_H\n\n#include <bw_ls2.h>\n#include <bw_hs2.h>\n#include <bw_peak.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::eq3band_plugin {\n\nclass Eq3bandPlugin : public InternalPlugin, public UidHelper<Eq3bandPlugin>\n{\npublic:\n    explicit Eq3bandPlugin(HostControl hostControl);\n\n    ~Eq3bandPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate{0};\n\n    FloatParameterValue* _lowshelf_freq;\n    FloatParameterValue* _lowshelf_gain;\n    FloatParameterValue* _lowshelf_q;\n    FloatParameterValue* _peak_freq;\n    FloatParameterValue* _peak_gain;\n    FloatParameterValue* _peak_q;\n    FloatParameterValue* _highshelf_freq;\n    FloatParameterValue* _highshelf_gain;\n    FloatParameterValue* _highshelf_q;\n\n    bw_ls2_coeffs _ls2_coeffs;\n    std::array<bw_ls2_state, MAX_TRACK_CHANNELS> _ls2_states;\n    bw_peak_coeffs _peak_coeffs;\n    std::array<bw_peak_state, MAX_TRACK_CHANNELS> _peak_states;\n    bw_hs2_coeffs _hs2_coeffs;\n    std::array<bw_hs2_state, MAX_TRACK_CHANNELS> _hs2_states;\n};\n\n} // namespace sushi::internal::eq3band_plugin\n\nELK_POP_WARNING\n\n#endif // EQ3BAND_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/flanger_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Flanger from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"flanger_plugin.h\"\n\nnamespace sushi::internal::flanger_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.flanger\";\nconstexpr auto DEFAULT_LABEL = \"Flanger\";\n\nconstexpr float FLANGER_AMOUNT_SCALE = 0.001f;\n\n\nFlangerPlugin::FlangerPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    // The low-level module bw_chorus.h exposes other parameters\n    // (delay & three coefficients for the direct / modulation / feedback branches)\n\n    // but the high-level flanger example pre-configures them accordingly to Dattoro's reccomendations\n    _rate = register_float_parameter(\"rate\", \"Rate\", \"Hz\",\n                                      1.0f, 0.01f, 2.0f,\n                                      Direction::AUTOMATABLE,\n                                      new CubicWarpPreProcessor(0.01f, 2.0f));\n    _amount = register_float_parameter(\"amount\", \"Amount\", \"\",\n                                       0.0f, 0.0f, 1.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    assert(_rate);\n    assert(_amount);\n}\n\nProcessorReturnCode FlangerPlugin::init(float sample_rate)\n{\n    // Default values taken from Brickworks example fx_flanger\n    bw_chorus_init(&_chorus_coeffs, 0.002f);\n    bw_chorus_set_delay(&_chorus_coeffs, 0.001f);\n    bw_chorus_set_coeff_x(&_chorus_coeffs, 0.7071f);\n    bw_chorus_set_coeff_mod(&_chorus_coeffs, 0.7071f);\n    bw_chorus_set_coeff_fb(&_chorus_coeffs, 0.7071f);\n    configure(sample_rate);\n\n    return ProcessorReturnCode::OK;\n}\n\nvoid FlangerPlugin::configure(float sample_rate)\n{\n    bw_chorus_set_sample_rate(&_chorus_coeffs, sample_rate);\n    return;\n}\n\nvoid FlangerPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_chorus_reset_coeffs(&_chorus_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        _delay_mem_areas[i].resize(bw_chorus_mem_req(&_chorus_coeffs));\n        bw_chorus_mem_set(&_chorus_coeffs, &_chorus_states[i], _delay_mem_areas[i].data());\n    }\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_chorus_reset_state(&_chorus_coeffs, &_chorus_states[i], 0.0f);\n    }\n}\n\nvoid FlangerPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid FlangerPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid FlangerPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_chorus_set_rate(&_chorus_coeffs, _rate->processed_value());\n    bw_chorus_set_amount(&_chorus_coeffs, _amount->processed_value() * FLANGER_AMOUNT_SCALE);\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_chorus_update_coeffs_ctrl(&_chorus_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_chorus_update_coeffs_audio(&_chorus_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                *out_channel_ptrs[i]++ = bw_chorus_process1(&_chorus_coeffs, &_chorus_states[i],\n                                                            *in_channel_ptrs[i]++);\n            }\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view FlangerPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::flanger_plugin\n\n"
  },
  {
    "path": "src/plugins/brickworks/flanger_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Flanger from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef FLANGER_PLUGIN_H\n#define FLANGER_PLUGIN_H\n\n#include <vector>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_SHORTEN_64_TO_32\nELK_DISABLE_CONVERSION_FROM_SIZE_T_TO_INT\n#include <bw_chorus.h>\nELK_POP_WARNING\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::flanger_plugin {\n\nclass FlangerPlugin : public InternalPlugin, public UidHelper<FlangerPlugin>\n{\npublic:\n    explicit FlangerPlugin(HostControl hostControl);\n\n    ~FlangerPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager{false, std::chrono::milliseconds(100)};\n    float _sample_rate{0};\n\n    FloatParameterValue* _rate;\n    FloatParameterValue* _amount;\n\n    bw_chorus_coeffs _chorus_coeffs;\n    std::array<bw_chorus_state, MAX_TRACK_CHANNELS> _chorus_states;\n    std::array<std::vector<std::byte>, MAX_TRACK_CHANNELS> _delay_mem_areas;\n};\n\n} // namespace sushi::internal::flanger_plugin\n\nELK_POP_WARNING\n\n#endif // FLANGER_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/fuzz_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Fuzz from Brickworks library, with internal 2x resampling\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"fuzz_plugin.h\"\n\nnamespace sushi::internal::fuzz_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.fuzz\";\nconstexpr auto DEFAULT_LABEL = \"Fuzz\";\n\n\nFuzzPlugin::FuzzPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _fuzz = register_float_parameter(\"fuzz\", \"Fuzz\", \"\",\n                                     0.0f, 0.0f, 1.0f,\n                                     Direction::AUTOMATABLE,\n                                     new FloatParameterPreProcessor(0.0f, 1.0f));\n    _volume = register_float_parameter(\"gain\", \"Gain\", \"\",\n                                       1.0f, 0.0f, 1.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    assert(_fuzz);\n    assert(_volume);\n}\n\nProcessorReturnCode FuzzPlugin::init(float sample_rate)\n{\n    bw_fuzz_init(&_fuzz_coeffs);\n    bw_src_int_init(&_src_up_coeffs, 2);\n    bw_src_int_init(&_src_down_coeffs, -2);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid FuzzPlugin::configure(float sample_rate)\n{\n    bw_fuzz_set_sample_rate(&_fuzz_coeffs, 2.0f * sample_rate);\n    return;\n}\n\nvoid FuzzPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_fuzz_reset_coeffs(&_fuzz_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_fuzz_reset_state(&_fuzz_coeffs, &_fuzz_states[i], 0.0f);\n        bw_src_int_reset_state(&_src_up_coeffs, &_src_up_states[i], 0.0f);\n        bw_src_int_reset_state(&_src_down_coeffs, &_src_down_states[i], 0.0f);\n    }\n}\n\nvoid FuzzPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid FuzzPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid FuzzPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_fuzz_set_fuzz(&_fuzz_coeffs, _fuzz->processed_value());\n    bw_fuzz_set_volume(&_fuzz_coeffs, _volume->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        bw_fuzz_update_coeffs_ctrl(&_fuzz_coeffs);\n        int n = 0;\n        while (n < AUDIO_CHUNK_SIZE)\n        {\n            // 2x upsample\n            int frames_left = std::min(AUDIO_CHUNK_SIZE - n, AUDIO_CHUNK_SIZE >> 1);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                bw_src_int_process(&_src_up_coeffs, &_src_up_states[i],\n                                   in_buffer.channel(i) + n, _tmp_buf.channel(i), frames_left);\n            }\n            // upsampled fuzz with coefficient interp.\n            int frames_upsample = frames_left << 1;\n            for (int n_up = 0; n_up < frames_upsample; n_up++)\n            {\n                bw_fuzz_update_coeffs_audio(&_fuzz_coeffs);\n                for (int i = 0; i < _current_input_channels; i++)\n                {\n                    float* buf = _tmp_buf.channel(i);\n                    buf[n_up] = bw_fuzz_process1(&_fuzz_coeffs, &_fuzz_states[i], buf[n_up]);\n                }\n            }\n            // 2x downsample\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                bw_src_int_process(&_src_down_coeffs, &_src_down_states[i],\n                                   _tmp_buf.channel(i), out_buffer.channel(i) + n, frames_upsample);\n            }\n            n += frames_left;\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view FuzzPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::fuzz_plugin\n\n"
  },
  {
    "path": "src/plugins/brickworks/fuzz_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Fuzz from Brickworks library, with internal 2x resampling\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef FUZZ_PLUGIN_H\n#define FUZZ_PLUGIN_H\n\n#include <bw_fuzz.h>\n#include <bw_src_int.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::fuzz_plugin {\n\nclass FuzzPlugin : public InternalPlugin, public UidHelper<FuzzPlugin>\n{\npublic:\n    explicit FuzzPlugin(HostControl hostControl);\n\n    ~FuzzPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate {0};\n\n    FloatParameterValue* _fuzz;\n    FloatParameterValue* _volume;\n\n    bw_fuzz_coeffs _fuzz_coeffs;\n    bw_src_int_coeffs _src_up_coeffs;\n    bw_src_int_coeffs _src_down_coeffs;\n    std::array<bw_fuzz_state, MAX_TRACK_CHANNELS> _fuzz_states;\n    std::array<bw_src_int_state, MAX_TRACK_CHANNELS> _src_up_states;\n    std::array<bw_src_int_state, MAX_TRACK_CHANNELS> _src_down_states;\n\n    ChunkSampleBuffer _tmp_buf {MAX_TRACK_CHANNELS};\n};\n\n} // namespace sushi::internal::fuzz_plugin\n\nELK_POP_WARNING\n\n#endif // FUZZ_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/highpass_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief HighPass from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"highpass_plugin.h\"\n\nnamespace sushi::internal::highpass_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.highpass\";\nconstexpr auto DEFAULT_LABEL = \"HighPass\";\n\n\nHighPassPlugin::HighPassPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _frequency = register_float_parameter(\"frequency\", \"Frequency\", \"Hz\",\n                                          50.0f, 20.0f, 20'000.0f,\n                                          Direction::AUTOMATABLE,\n                                          new CubicWarpPreProcessor(20.0f, 20'000.0f));\n\n    assert(_frequency);\n}\n\nProcessorReturnCode HighPassPlugin::init(float sample_rate)\n{\n    bw_hp1_init(&_hp1_coeffs);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid HighPassPlugin::configure(float sample_rate)\n{\n    bw_hp1_set_sample_rate(&_hp1_coeffs, sample_rate);\n    return;\n}\n\nvoid HighPassPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_hp1_reset_coeffs(&_hp1_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_hp1_reset_state(&_hp1_coeffs, &_hp1_states[i], 0.0f);\n    }\n}\n\nvoid HighPassPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid HighPassPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid HighPassPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_hp1_set_cutoff(&_hp1_coeffs, _frequency->processed_value());\n\n    if (!_bypassed)\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_hp1_update_coeffs_ctrl(&_hp1_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_hp1_update_coeffs_audio(&_hp1_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                *out_channel_ptrs[i]++ = bw_hp1_process1(&_hp1_coeffs, &_hp1_states[i],\n                                                         *in_channel_ptrs[i]++);\n            }\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view HighPassPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::highpass_plugin\n\n"
  },
  {
    "path": "src/plugins/brickworks/highpass_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief HighPass from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef HIGHPASS_PLUGIN_H\n#define HIGHPASS_PLUGIN_H\n\n#include <bw_hp1.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::highpass_plugin {\n\nclass HighPassPlugin : public InternalPlugin, public UidHelper<HighPassPlugin>\n{\npublic:\n    explicit HighPassPlugin(HostControl hostControl);\n\n    ~HighPassPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate{0};\n\n    FloatParameterValue* _frequency;\n\n    bw_hp1_coeffs _hp1_coeffs;\n    std::array<bw_hp1_state, MAX_TRACK_CHANNELS> _hp1_states;\n};\n\n} // namespace sushi::internal::highpass_plugin\n\nELK_POP_WARNING\n\n#endif // HIGHPASS_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/multi_filter_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief 2nd-order multimode filter from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"multi_filter_plugin.h\"\n\nnamespace sushi::internal::multi_filter_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.multi_filter\";\nconstexpr auto DEFAULT_LABEL = \"MultiFilter\";\n\n\nMultiFilterPlugin::MultiFilterPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _frequency = register_float_parameter(\"frequency\", \"Frequency\", \"Hz\",\n                                          1'000.0f, 20.0f, 20'000.0f,\n                                          Direction::AUTOMATABLE,\n                                          new CubicWarpPreProcessor(20.0f, 20'000.0f));\n    _Q = register_float_parameter(\"Q\", \"Q\", \"\",\n                                  1.0f, 0.5f, 10.0f,\n                                  Direction::AUTOMATABLE,\n                                  new FloatParameterPreProcessor(0.5f, 10.0f));\n    _input_coeff = register_float_parameter(\"input_coeff\", \"Input coefficient\", \"\",\n                                            1.0f, -1.0f, 1.0f,\n                                            Direction::AUTOMATABLE,\n                                            new FloatParameterPreProcessor(-1.0f, 1.0f));\n    _lowpass_coeff = register_float_parameter(\"lowpass_coeff\", \"Lowpass coefficient\", \"\",\n                                              0.0f, -1.0f, 1.0f,\n                                              Direction::AUTOMATABLE,\n                                              new FloatParameterPreProcessor(-1.0f, 1.0f));\n    _bandpass_coeff = register_float_parameter(\"bandpass_coeff\", \"Bandpass coefficient\", \"\",\n                                               0.0f, -1.0f, 1.0f,\n                                               Direction::AUTOMATABLE,\n                                               new FloatParameterPreProcessor(-1.0f, 1.0f));\n    _highpass_coeff = register_float_parameter(\"highpass_coeff\", \"Highpass coefficient\", \"\",\n                                               0.0f, -1.0f, 1.0f,\n                                               Direction::AUTOMATABLE,\n                                               new FloatParameterPreProcessor(-1.0f, 1.0f));\n\n    assert(_frequency);\n    assert(_Q);\n    assert(_input_coeff);\n    assert(_lowpass_coeff);\n    assert(_bandpass_coeff);\n    assert(_highpass_coeff);\n}\n\nProcessorReturnCode MultiFilterPlugin::init(float sample_rate)\n{\n    bw_mm2_init(&_mm2_coeffs);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid MultiFilterPlugin::configure(float sample_rate)\n{\n    bw_mm2_set_sample_rate(&_mm2_coeffs, sample_rate);\n    return;\n}\n\nvoid MultiFilterPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_mm2_reset_coeffs(&_mm2_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_mm2_reset_state(&_mm2_coeffs, &_mm2_states[i], 0.0f);\n    }\n}\n\nvoid MultiFilterPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid MultiFilterPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid MultiFilterPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_mm2_set_cutoff(&_mm2_coeffs, _frequency->processed_value());\n    bw_mm2_set_Q(&_mm2_coeffs, _Q->processed_value());\n    bw_mm2_set_coeff_x(&_mm2_coeffs, _input_coeff->processed_value());\n    bw_mm2_set_coeff_lp(&_mm2_coeffs, _lowpass_coeff->processed_value());\n    bw_mm2_set_coeff_bp(&_mm2_coeffs, _bandpass_coeff->processed_value());\n    bw_mm2_set_coeff_hp(&_mm2_coeffs, _highpass_coeff->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_mm2_update_coeffs_ctrl(&_mm2_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_mm2_update_coeffs_audio(&_mm2_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                *out_channel_ptrs[i]++ = bw_mm2_process1(&_mm2_coeffs, &_mm2_states[i],\n                                                         *in_channel_ptrs[i]++);\n            }\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view MultiFilterPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::multi_filter_plugin\n\n\n"
  },
  {
    "path": "src/plugins/brickworks/multi_filter_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief 2nd-order multimode filter from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef MULTIFILTER_PLUGIN_H\n#define MULTIFILTER_PLUGIN_H\n\n#include <bw_mm2.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::multi_filter_plugin {\n\nclass MultiFilterPlugin : public InternalPlugin, public UidHelper<MultiFilterPlugin>\n{\npublic:\n    explicit MultiFilterPlugin(HostControl hostControl);\n\n    ~MultiFilterPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate{0};\n\n    FloatParameterValue* _frequency;\n    FloatParameterValue* _Q;\n    FloatParameterValue* _input_coeff;\n    FloatParameterValue* _lowpass_coeff;\n    FloatParameterValue* _bandpass_coeff;\n    FloatParameterValue* _highpass_coeff;\n\n    bw_mm2_coeffs _mm2_coeffs;\n    std::array<bw_mm2_state, MAX_TRACK_CHANNELS> _mm2_states;\n};\n\n} // namespace sushi::internal::multi_filter_plugin\n\nELK_POP_WARNING\n\n#endif // MULTIFILTER_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/noise_gate_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Noise gate from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"noise_gate_plugin.h\"\n\nnamespace sushi::internal::noise_gate_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.noise_gate\";\nconstexpr auto DEFAULT_LABEL = \"Noise gate\";\n\n\nNoiseGatePlugin::NoiseGatePlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _threshold = register_float_parameter(\"threshold\", \"Threshold\", \"dB\",\n                                      0.0f, -60.0f, 0.0f,\n                                      Direction::AUTOMATABLE,\n                                      new dBToLinPreProcessor(-60.0f, 0.0f));\n    _ratio = register_float_parameter(\"ratio\", \"Inverse ratio\", \"\",\n                                      0.0f, 0.0f, 1.0f,\n                                      Direction::AUTOMATABLE,\n                                      new FloatParameterPreProcessor(0.0f, 1.0f));\n    _attack = register_float_parameter(\"attack\", \"Attack time\", \"sec\",\n                                       0.0f, 0.0f, 1.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(0.0f, 1.0f));\n    _release = register_float_parameter(\"release\", \"Release time\", \"sec\",\n                                        0.0f, 0.0f, 1.0f,\n                                        Direction::AUTOMATABLE,\n                                        new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    assert(_threshold);\n    assert(_ratio);\n    assert(_attack);\n    assert(_release);\n}\n\nProcessorReturnCode NoiseGatePlugin::init(float sample_rate)\n{\n    bw_noise_gate_init(&_noise_gate_coeffs);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid NoiseGatePlugin::configure(float sample_rate)\n{\n    bw_noise_gate_set_sample_rate(&_noise_gate_coeffs, sample_rate);\n}\n\nvoid NoiseGatePlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_noise_gate_reset_coeffs(&_noise_gate_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_noise_gate_reset_state(&_noise_gate_coeffs, &_noise_gate_states[i], 0.0f, 0.0f);\n    }\n}\n\nvoid NoiseGatePlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid NoiseGatePlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid NoiseGatePlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_noise_gate_set_thresh_lin(&_noise_gate_coeffs, _threshold->processed_value());\n    float ratio_inv = _ratio->processed_value();\n    // the Brickworks example uses the INFINITY constant for the upper limit,\n    // here only slightly above the previous values\n    float ratio = (ratio_inv < 0.999f) ? 1.0f / (1.0f - ratio_inv) : 1.0f / (1.0f - 0.9999f);\n    bw_noise_gate_set_ratio(&_noise_gate_coeffs, ratio);\n    bw_noise_gate_set_attack_tau(&_noise_gate_coeffs, _attack->processed_value());\n    bw_noise_gate_set_release_tau(&_noise_gate_coeffs, _release->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_noise_gate_update_coeffs_ctrl(&_noise_gate_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_noise_gate_update_coeffs_audio(&_noise_gate_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                float x = *in_channel_ptrs[i]++;\n                *out_channel_ptrs[i]++ = bw_noise_gate_process1(&_noise_gate_coeffs, &_noise_gate_states[i],\n                                                                x, x);\n            }\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view NoiseGatePlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::noise_gate_plugin\n"
  },
  {
    "path": "src/plugins/brickworks/noise_gate_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Noise gate from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef NOISE_GATE_PLUGIN_H\n#define NOISE_GATE_PLUGIN_H\n\n#include <bw_noise_gate.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::noise_gate_plugin {\n\nclass NoiseGatePlugin : public InternalPlugin, public UidHelper<NoiseGatePlugin>\n{\npublic:\n    explicit NoiseGatePlugin(HostControl hostControl);\n\n    ~NoiseGatePlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate {0};\n\n    FloatParameterValue* _threshold;\n    FloatParameterValue* _ratio;\n    FloatParameterValue* _attack;\n    FloatParameterValue* _release;\n\n    bw_noise_gate_coeffs _noise_gate_coeffs {};\n    std::array<bw_noise_gate_state, MAX_TRACK_CHANNELS> _noise_gate_states {};\n};\n\n} // namespace sushi::internal::noise_gate_plugin\n\nELK_POP_WARNING\n\n#endif // NOISE_GATE_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/notch_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Notch from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"notch_plugin.h\"\n\nnamespace sushi::internal::notch_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.notch\";\nconstexpr auto DEFAULT_LABEL = \"Notch\";\n\n\nNotchPlugin::NotchPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _frequency = register_float_parameter(\"frequency\", \"Frequency\", \"Hz\",\n                                          1'000.0f, 20.0f, 20'000.0f,\n                                          Direction::AUTOMATABLE,\n                                          new CubicWarpPreProcessor(20.0f, 20'000.0f));\n    _Q = register_float_parameter(\"Q\", \"Q\", \"\",\n                                  1.0f, 0.5f, 10.0f,\n                                  Direction::AUTOMATABLE,\n                                  new FloatParameterPreProcessor(0.5f, 10.0f));\n\n    assert(_frequency);\n    assert(_Q);\n}\n\nProcessorReturnCode NotchPlugin::init(float sample_rate)\n{\n    bw_notch_init(&_notch_coeffs);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid NotchPlugin::configure(float sample_rate)\n{\n    bw_notch_set_sample_rate(&_notch_coeffs, sample_rate);\n}\n\nvoid NotchPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_notch_reset_coeffs(&_notch_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_notch_reset_state(&_notch_coeffs, &_notch_states[i], 0.0f);\n    }\n}\n\nvoid NotchPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid NotchPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid NotchPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_notch_set_cutoff(&_notch_coeffs, _frequency->processed_value());\n    bw_notch_set_Q(&_notch_coeffs, _Q->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_notch_update_coeffs_ctrl(&_notch_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_notch_update_coeffs_audio(&_notch_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                *out_channel_ptrs[i]++ = bw_notch_process1(&_notch_coeffs, &_notch_states[i],\n                                                            *in_channel_ptrs[i]++);\n            }\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view NotchPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::notch_plugin\n"
  },
  {
    "path": "src/plugins/brickworks/notch_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Notch from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef NOTCH_PLUGIN_H\n#define NOTCH_PLUGIN_H\n\n#include <bw_notch.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::notch_plugin {\n\nclass NotchPlugin : public InternalPlugin, public UidHelper<NotchPlugin>\n{\npublic:\n    explicit NotchPlugin(HostControl hostControl);\n\n    ~NotchPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate {0};\n\n    FloatParameterValue* _frequency;\n    FloatParameterValue* _Q;\n\n    bw_notch_coeffs _notch_coeffs {};\n    std::array<bw_notch_state, MAX_TRACK_CHANNELS> _notch_states {};\n};\n\n} // namespace sushi::internal::notch_plugin\n\nELK_POP_WARNING\n\n#endif // NOTCH_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/phaser_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Phaser from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"phaser_plugin.h\"\n\nnamespace sushi::internal::phaser_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.phaser\";\nconstexpr auto DEFAULT_LABEL = \"Phaser\";\n\n\nPhaserPlugin::PhaserPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _rate = register_float_parameter(\"rate\", \"Rate\", \"Hz\",\n                                      1.0f, 0.5f, 5.0f,\n                                      Direction::AUTOMATABLE,\n                                      new CubicWarpPreProcessor(0.5f, 5.0f));\n    _center = register_float_parameter(\"center\", \"Center Frequency\", \"Hz\",\n                                      1'000.0f, 100.0f, 10'000.0f,\n                                      Direction::AUTOMATABLE,\n                                      new CubicWarpPreProcessor(100.0f, 10'000.0f));\n    _amount = register_float_parameter(\"amount\", \"Amount\", \"oct\",\n                                       1.0f, 0.0f, 4.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(0.0f, 4.0f));\n\n    assert(_rate);\n    assert(_center);\n    assert(_amount);\n}\n\nProcessorReturnCode PhaserPlugin::init(float sample_rate)\n{\n    bw_phaser_init(&_phaser_coeffs);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid PhaserPlugin::configure(float sample_rate)\n{\n    bw_phaser_set_sample_rate(&_phaser_coeffs, sample_rate);\n    return;\n}\n\nvoid PhaserPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_phaser_reset_coeffs(&_phaser_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_phaser_reset_state(&_phaser_coeffs, &_phaser_states[i], 0.0f);\n    }\n}\n\nvoid PhaserPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid PhaserPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid PhaserPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_phaser_set_rate(&_phaser_coeffs, _rate->processed_value());\n    bw_phaser_set_center(&_phaser_coeffs, _center->processed_value());\n    bw_phaser_set_amount(&_phaser_coeffs, _amount->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_phaser_update_coeffs_ctrl(&_phaser_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_phaser_update_coeffs_audio(&_phaser_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                *out_channel_ptrs[i]++ = bw_phaser_process1(&_phaser_coeffs, &_phaser_states[i],\n                                                            *in_channel_ptrs[i]++);\n            }\n        }\n\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view PhaserPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::phaser_plugin\n"
  },
  {
    "path": "src/plugins/brickworks/phaser_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Phaser from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef PHASER_PLUGIN_H\n#define PHASER_PLUGIN_H\n\n#include <bw_phaser.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::phaser_plugin {\n\nclass PhaserPlugin : public InternalPlugin, public UidHelper<PhaserPlugin>\n{\npublic:\n    explicit PhaserPlugin(HostControl hostControl);\n\n    ~PhaserPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager{false, std::chrono::milliseconds(30)};\n    float _sample_rate{0};\n\n    FloatParameterValue* _rate;\n    FloatParameterValue* _center;\n    FloatParameterValue* _amount;\n\n    bw_phaser_coeffs _phaser_coeffs;\n    std::array<bw_phaser_state, MAX_TRACK_CHANNELS> _phaser_states;\n};\n\n} // namespace sushi::internal::phaser_plugin\n\nELK_POP_WARNING\n\n#endif // PHASER_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/ring_mod_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Ring modulator from Brickworks library\n * @copyright 2017-2025, Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n#include <cmath>\n\n#include <bw_osc_sin.h>\n\n#include \"ring_mod_plugin.h\"\n\nnamespace sushi::internal::ring_mod_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.ring_mod\";\nconstexpr auto DEFAULT_LABEL = \"Ring Modulator\";\n\n\nRingModPlugin::RingModPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _frequency = register_float_parameter(\"frequency\", \"Frequency\", \"Hz\",\n                                          1000.0f, 20.0f, 20000.0f,\n                                          Direction::AUTOMATABLE,\n                                          new CubicWarpPreProcessor(20.0f, 20000.0f));\n    _amount = register_float_parameter(\"amount\", \"Amount\", \"\",\n                                       0.0f, -1.0f, 1.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(-1.0f, 1.0f));\n\n    assert(_frequency);\n    assert(_amount);\n}\n\nProcessorReturnCode RingModPlugin::init(float sample_rate)\n{\n    bw_phase_gen_init(&_phase_gen_coeffs);\n    bw_phase_gen_set_sample_rate(&_phase_gen_coeffs, sample_rate);\n    bw_ring_mod_init(&_ring_mod_coeffs);\n    bw_ring_mod_set_sample_rate(&_ring_mod_coeffs, sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid RingModPlugin::configure(float sample_rate)\n{\n    bw_phase_gen_set_sample_rate(&_phase_gen_coeffs, sample_rate);\n    bw_ring_mod_set_sample_rate(&_ring_mod_coeffs, sample_rate);\n    return;\n}\n\nvoid RingModPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_phase_gen_reset_coeffs(&_phase_gen_coeffs);\n    bw_ring_mod_reset_coeffs(&_ring_mod_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        float v, v_inc;\n        bw_phase_gen_reset_state(&_phase_gen_coeffs, &_phase_gen_state[i], 0.0f, &v, &v_inc);\n    }\n}\n\nvoid RingModPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid RingModPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid RingModPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_phase_gen_set_frequency(&_phase_gen_coeffs, _frequency->processed_value());\n    bw_ring_mod_set_amount(&_ring_mod_coeffs, _amount->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_phase_gen_update_coeffs_ctrl(&_phase_gen_coeffs);\n        bw_ring_mod_update_coeffs_ctrl(&_ring_mod_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_phase_gen_update_coeffs_audio(&_phase_gen_coeffs);\n            bw_ring_mod_update_coeffs_audio(&_ring_mod_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                float phase, phase_inc;\n                bw_phase_gen_process1(&_phase_gen_coeffs, &_phase_gen_state[i], &phase, &phase_inc);\n                float modulator = bw_osc_sin_process1(phase);\n                *out_channel_ptrs[i]++ = bw_ring_mod_process1(&_ring_mod_coeffs, *in_channel_ptrs[i]++, modulator);\n            }\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view RingModPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::ring_mod_plugin\n"
  },
  {
    "path": "src/plugins/brickworks/ring_mod_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Ring modulator from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef RING_MOD_PLUGIN_H\n#define RING_MOD_PLUGIN_H\n\n#include <bw_ring_mod.h>\n#include <bw_phase_gen.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::ring_mod_plugin {\n\nclass RingModPlugin : public InternalPlugin, public UidHelper<RingModPlugin>\n{\npublic:\n    explicit RingModPlugin(HostControl hostControl);\n\n    ~RingModPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate{0};\n\n    FloatParameterValue* _frequency;\n    FloatParameterValue* _amount;\n\n    bw_phase_gen_coeffs _phase_gen_coeffs;\n    std::array<bw_phase_gen_state, MAX_TRACK_CHANNELS> _phase_gen_state;\n    bw_ring_mod_coeffs _ring_mod_coeffs;\n};\n\n} // namespace sushi::internal::ring_mod_plugin\n\nELK_POP_WARNING\n\n#endif // RING_MOD_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/saturation_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Saturation from Brickworks library, with internal 2x resampling\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"saturation_plugin.h\"\n\nnamespace sushi::internal::saturation_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.saturation\";\nconstexpr auto DEFAULT_LABEL = \"Saturation\";\n\n\nSaturationPlugin::SaturationPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _bias = register_float_parameter(\"bias\", \"Bias\", \"\",\n                                     0.0f, -2.5f, 2.5f,\n                                     Direction::AUTOMATABLE,\n                                     new FloatParameterPreProcessor(-2.5f, 2.5f));\n    _gain = register_float_parameter(\"gain\", \"Gain\", \"\",\n                                     1.0f, 0.1f, 10.0f,\n                                     Direction::AUTOMATABLE,\n                                     new CubicWarpPreProcessor(0.1f, 10.0f));\n\n    assert(_bias);\n    assert(_gain);\n}\n\nProcessorReturnCode SaturationPlugin::init(float sample_rate)\n{\n    bw_satur_init(&_saturation_coeffs);\n    bw_src_int_init(&_src_up_coeffs, 2);\n    bw_src_int_init(&_src_down_coeffs, -2);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid SaturationPlugin::configure(float sample_rate)\n{\n    bw_satur_set_sample_rate(&_saturation_coeffs, 2.0f * sample_rate);\n    return;\n}\n\nvoid SaturationPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_satur_reset_coeffs(&_saturation_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_satur_reset_state(&_saturation_coeffs, &_saturation_states[i], 0.0f);\n        bw_src_int_reset_state(&_src_up_coeffs, &_src_up_states[i], 0.0f);\n        bw_src_int_reset_state(&_src_down_coeffs, &_src_down_states[i], 0.0f);\n    }\n}\n\nvoid SaturationPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid SaturationPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid SaturationPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_satur_set_bias(&_saturation_coeffs, _bias->processed_value());\n    bw_satur_set_gain(&_saturation_coeffs, _gain->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        bw_satur_update_coeffs_ctrl(&_saturation_coeffs);\n        int n = 0;\n        while (n < AUDIO_CHUNK_SIZE)\n        {\n            // 2x upsample\n            int frames_left = std::min(AUDIO_CHUNK_SIZE - n, AUDIO_CHUNK_SIZE >> 1);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                bw_src_int_process(&_src_up_coeffs, &_src_up_states[i],\n                                   in_buffer.channel(i) + n, _tmp_buf.channel(i), frames_left);\n            }\n            // upsampled saturation with coefficient interp.\n            int frames_upsample = frames_left << 1;\n            for (int n_up = 0; n_up < frames_upsample; n_up++)\n            {\n                bw_satur_update_coeffs_audio(&_saturation_coeffs);\n                for (int i = 0; i < _current_input_channels; i++)\n                {\n                    float* buf = _tmp_buf.channel(i);\n                    // we default to the version without gain compensation\n                    buf[n_up] = bw_satur_process1(&_saturation_coeffs, &_saturation_states[i], buf[n_up]);\n                }\n            }\n            // 2x downsample\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                bw_src_int_process(&_src_down_coeffs, &_src_down_states[i],\n                                   _tmp_buf.channel(i), out_buffer.channel(i) + n, frames_upsample);\n            }\n            n += frames_left;\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view SaturationPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::saturation_plugin\n\n\n"
  },
  {
    "path": "src/plugins/brickworks/saturation_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Saturation from Brickworks library, with internal 2x resampling\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef SATURATION_PLUGIN_H\n#define SATURATION_PLUGIN_H\n\n#include <bw_satur.h>\n#include <bw_src_int.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::saturation_plugin {\n\nclass SaturationPlugin : public InternalPlugin, public UidHelper<SaturationPlugin>\n{\npublic:\n    explicit SaturationPlugin(HostControl hostControl);\n\n    ~SaturationPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager;\n    float _sample_rate{0};\n\n    FloatParameterValue* _bias;\n    FloatParameterValue* _gain;\n\n    bw_satur_coeffs _saturation_coeffs;\n    bw_src_int_coeffs _src_up_coeffs;\n    bw_src_int_coeffs _src_down_coeffs;\n    std::array<bw_satur_state, MAX_TRACK_CHANNELS> _saturation_states;\n    std::array<bw_src_int_state, MAX_TRACK_CHANNELS> _src_up_states;\n    std::array<bw_src_int_state, MAX_TRACK_CHANNELS> _src_down_states;\n\n    ChunkSampleBuffer _tmp_buf{MAX_TRACK_CHANNELS};\n};\n\n} // namespace sushi::internal::saturation_plugin\n\nELK_POP_WARNING\n\n#endif // SATURATION_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/simple_synth_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB, Stockholm\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Simple monophonic synthesizer from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <bw_buf.h>\n\n#include \"elklog/static_logger.h\"\n\n#include \"simple_synth_plugin.h\"\n\n#include <iostream>\n\nnamespace sushi::internal::simple_synth_plugin {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"simplesynth\");\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.simple_synth\";\nconstexpr auto DEFAULT_LABEL = \"Simple synthesizer\";\n\nconstexpr float A4_FREQUENCY = 440.0f;\nconstexpr int A4_NOTENUM = 69;\nconstexpr float NOTE2FREQ_SCALE = 5.0f / 60.0f;\n\n\nSimpleSynthPlugin::SimpleSynthPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _volume = register_float_parameter(\"volume\", \"Volume\", \"dB\",\n                                       0.0f, -60.0f, 12.0f,\n                                       Direction::AUTOMATABLE,\n                                       new dBToLinPreProcessor(-60.0f, 12.0f));\n\n    // scaling taken from newer Brickworks synth examples\n\n    constexpr float MAX_PORTAMENTO = 0.43429448190325173f;\n    _portamento = register_float_parameter(\"portamento\", \"Portamento time\", \"sec\",\n                                           0.0f, 0.0f, MAX_PORTAMENTO,\n                                           Direction::AUTOMATABLE,\n                                           new FloatParameterPreProcessor(0.0f, MAX_PORTAMENTO));\n    _pulse_width = register_float_parameter(\"pulse_width\", \"Pulse width\", \"\",\n                                            0.5f, 0.0f, 1.0f,\n                                            Direction::AUTOMATABLE,\n                                            new FloatParameterPreProcessor(0.0f, 1.0f));\n    _filter_cutoff = register_float_parameter(\"filter_cutoff\", \"Filter cutoff\", \"Hz\",\n                                              4'000.0f, 20.0f, 20'000.0f,\n                                              Direction::AUTOMATABLE,\n                                              new CubicWarpPreProcessor(20.0f, 20'000.0f));\n    _filter_Q = register_float_parameter(\"filter_Q\", \"Filter Q\", \"\",\n                                         1.0f, 0.5f, 10.0f,\n                                         Direction::AUTOMATABLE,\n                                         new FloatParameterPreProcessor(0.5f, 10.0f));\n    _attack = register_float_parameter(\"attack\", \"Attack time\", \"sec\",\n                                       0.002f, 0.002f, 1.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(0.002f, 1.0f));\n    _decay = register_float_parameter(\"decay\", \"Decay time\", \"sec\",\n                                      0.002f, 0.002f, 1.0f,\n                                      Direction::AUTOMATABLE,\n                                      new FloatParameterPreProcessor(0.002f, 1.0f));\n    _sustain = register_float_parameter(\"sustain\", \"Sustain level\", \"\",\n                                        1.0f, 0.0f, 1.0f,\n                                        Direction::AUTOMATABLE,\n                                        new FloatParameterPreProcessor(0.0f, 1.0f));\n    _release = register_float_parameter(\"release\", \"Release time\", \"sec\",\n                                        0.002f, 0.002f, 1.0f,\n                                        Direction::AUTOMATABLE,\n                                        new FloatParameterPreProcessor(0.002f, 1.0f));\n\n    _max_input_channels = 0;\n};\n\nProcessorReturnCode SimpleSynthPlugin::init(float sample_rate)\n{\n    bw_phase_gen_init(&_phase_gen_coeffs);\n    bw_osc_pulse_init(&_osc_pulse_coeffs);\n    bw_svf_init(&_svf_coeffs);\n    bw_env_gen_init(&_env_gen_coeffs);\n\n    bw_osc_pulse_set_antialiasing(&_osc_pulse_coeffs, 1);\n\n    configure(sample_rate);\n\n    return ProcessorReturnCode::OK;\n}\n\nvoid SimpleSynthPlugin::configure(float sample_rate)\n{\n    bw_phase_gen_set_sample_rate(&_phase_gen_coeffs, sample_rate);\n    bw_osc_pulse_set_sample_rate(&_osc_pulse_coeffs, sample_rate);\n    bw_svf_set_sample_rate(&_svf_coeffs, sample_rate);\n    bw_env_gen_set_sample_rate(&_env_gen_coeffs, sample_rate);\n}\n\nvoid SimpleSynthPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n\n    // Initialize note tracking\n    for (int i = 0; i < MAX_MIDI_NOTE; i++)\n    {\n        _held_notes[i] = false;\n    }\n    _note = A4_NOTENUM;\n    _freq = A4_FREQUENCY;\n    _gate = 0;\n\n    // Reset all brickworks modules\n    bw_phase_gen_reset_coeffs(&_phase_gen_coeffs);\n    float phase_out, phase_inc_out;\n    bw_phase_gen_reset_state(&_phase_gen_coeffs, &_phase_gen_state, 0.0f, &phase_out, &phase_inc_out);\n\n    bw_osc_pulse_reset_coeffs(&_osc_pulse_coeffs);\n    bw_osc_filt_reset_state(&_osc_filt_state, 0.0f);\n\n    bw_svf_reset_coeffs(&_svf_coeffs);\n    float svf_lp, svf_bp, svf_hp;\n    bw_svf_reset_state(&_svf_coeffs, &_svf_state, 0.0f, &svf_lp, &svf_bp, &svf_hp);\n\n    bw_env_gen_reset_coeffs(&_env_gen_coeffs);\n    bw_env_gen_reset_state(&_env_gen_coeffs, &_env_gen_state, 0);\n}\n\n\nvoid SimpleSynthPlugin::process_event(const RtEvent& event)\n{\n    // Just forward everything to the process() callback with a FIFO,\n    // to correctly handle sample-accurate events\n    switch (event.type())\n    {\n        case RtEventType::NOTE_ON:\n        case RtEventType::NOTE_OFF:\n        {\n            if (_bypassed)\n            {\n                break;\n            }\n            if (!_event_fifo.push(event))\n            {\n                ELKLOG_LOG_ERROR(\"Internal queue full while processing event\");\n            }\n            break;\n        }\n\n        case RtEventType::NOTE_AFTERTOUCH:\n        case RtEventType::PITCH_BEND:\n        case RtEventType::AFTERTOUCH:\n        case RtEventType::MODULATION:\n        case RtEventType::WRAPPED_MIDI_EVENT:\n            // Consume these events so they are not propagated\n            break;\n\n        default:\n            InternalPlugin::process_event(event);\n            break;\n    }\n}\n\n\nvoid SimpleSynthPlugin::process_audio(const ChunkSampleBuffer& /* in_buffer */, ChunkSampleBuffer& out_buffer)\n{\n    out_buffer.clear();\n    bw_phase_gen_set_portamento_tau(&_phase_gen_coeffs, _portamento->processed_value());\n    bw_osc_pulse_set_pulse_width(&_osc_pulse_coeffs, _pulse_width->processed_value());\n    bw_svf_set_cutoff(&_svf_coeffs, _filter_cutoff->processed_value());\n    bw_svf_set_Q(&_svf_coeffs, _filter_Q->processed_value());\n    bw_env_gen_set_attack(&_env_gen_coeffs, _attack->processed_value());\n    bw_env_gen_set_decay(&_env_gen_coeffs, _decay->processed_value());\n    bw_env_gen_set_sustain(&_env_gen_coeffs, _sustain->processed_value());\n    bw_env_gen_set_release(&_env_gen_coeffs, _release->processed_value());\n\n    int previous_offset = 0;\n    RtEvent event;\n\n    while (_event_fifo.pop(event))\n    {\n        int next_offset = event.sample_offset();\n        // we're assuming that events are received in order,\n        // if that's not the case simply drop the event\n        if (next_offset < previous_offset)\n        {\n            ELKLOG_LOG_DEBUG(\"Dropping unordered event of type {} with sample offset {}\",\n                             static_cast<int>(event.type()), event.sample_offset());\n            continue;\n        }\n        _render_loop(previous_offset, next_offset);\n\n        auto key_event = event.keyboard_event();\n        int note = key_event->note();\n        switch (key_event->type())\n        {\n        case RtEventType::NOTE_ON:\n        {\n            ELKLOG_LOG_DEBUG(\"Note ON, num. {}, vel. {}\",\n                             note, key_event->velocity());\n            _held_notes[note] = true;\n            _update_note_gate();\n            break;\n        }\n\n        case RtEventType::NOTE_OFF:\n        {\n            ELKLOG_LOG_DEBUG(\"Note OFF, num. {}, vel. {}\",\n                            note, key_event->velocity());\n            if (_held_notes[note])\n            {\n                _held_notes[note] = false;\n                _update_note_gate();\n            }\n            break;\n        }\n\n        default:\n            ELKLOG_LOG_DEBUG(\"Unexpected event type passed to process(): {}\", static_cast<int>(key_event->type()));\n\n        }\n        previous_offset = next_offset;\n    }\n\n    // Process any remaining samples\n    int remaining = AUDIO_CHUNK_SIZE - previous_offset;\n    if (remaining > 0)\n    {\n        _render_loop(previous_offset, remaining);\n    }\n\n    if (!_bypassed)\n    {\n        float gain = _volume->processed_value();\n        out_buffer.add_with_gain(_render_buffer, gain);\n    }\n}\n\nvoid SimpleSynthPlugin::_update_note_gate()\n{\n    for (int i = MAX_MIDI_NOTE-1; i >= 0; i--)\n    {\n        if (_held_notes[i])\n        {\n            _note = i;\n            _gate = 1;\n            // the Brickworks example also had other tuning parameters, skipped here and precomputing a fixed freq instead\n            _freq = A4_FREQUENCY * bw_pow2f(NOTE2FREQ_SCALE * static_cast<float>(_note - A4_NOTENUM));\n            return;\n        }\n    }\n    _gate = 0;\n\n}\n\nvoid SimpleSynthPlugin::_render_loop(int offset, int n)\n{\n    float* out = &_render_buffer.channel(0)[offset];\n    float* buf = &_aux_buffer.channel(0)[offset];\n\n\tconst bool vca_open = bw_env_gen_get_phase(&_env_gen_state) != bw_env_gen_phase_off || _gate;\n\n    bw_phase_gen_set_frequency(&_phase_gen_coeffs, _freq);\n    bw_phase_gen_process(&_phase_gen_coeffs, &_phase_gen_state, nullptr, out, buf, n);\n    bw_osc_pulse_process(&_osc_pulse_coeffs, out, buf, out, n);\n    bw_osc_filt_process(&_osc_filt_state, out, out, n);\n    bw_svf_process(&_svf_coeffs, &_svf_state, out, out, nullptr, nullptr, n);\n    if (vca_open)\n    {\n        bw_env_gen_process(&_env_gen_coeffs, &_env_gen_state, _gate, buf, n);\n        bw_buf_mul(out, out, buf, n);\n    }\n    else\n    {\n        bw_buf_fill(0.0f, out, n);\n    }\n}\n\nstd::string_view SimpleSynthPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::simple_synth_plugin\n"
  },
  {
    "path": "src/plugins/brickworks/simple_synth_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB, Stockholm\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Simple monophonic synthesizer from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SIMPLE_SYNTH_PLUGIN_H\n#define SUSHI_SIMPLE_SYNTH_PLUGIN_H\n\n#include <array>\n\n#include <bw_math.h>\n#include <bw_phase_gen.h>\n#include <bw_osc_pulse.h>\n#include <bw_osc_filt.h>\n#include <bw_svf.h>\n#include <bw_env_gen.h>\n#include <bw_gain.h>\n\n#include \"library/internal_plugin.h\"\n#include \"library/rt_event_fifo.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::simple_synth_plugin {\n\nconstexpr int MAX_MIDI_NOTE = 128;\n\nclass Accessor;\n\nclass SimpleSynthPlugin : public InternalPlugin, public UidHelper<SimpleSynthPlugin>\n{\npublic:\n    explicit SimpleSynthPlugin(HostControl host_control);\n\n    ~SimpleSynthPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void process_event(const RtEvent& event) override ;\n\n    void process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    friend Accessor;\n\n    void _render_loop(int offset, int n);\n\n    void _update_note_gate();\n\n    ChunkSampleBuffer _render_buffer {1};\n    ChunkSampleBuffer _aux_buffer {1};\n\n    FloatParameterValue* _volume;\n    FloatParameterValue* _portamento;\n    FloatParameterValue* _pulse_width;\n    FloatParameterValue* _filter_cutoff;\n    FloatParameterValue* _filter_Q;\n    FloatParameterValue* _attack;\n    FloatParameterValue* _decay;\n    FloatParameterValue* _sustain;\n    FloatParameterValue* _release;\n\n    bw_phase_gen_coeffs _phase_gen_coeffs {};\n    bw_phase_gen_state  _phase_gen_state {};\n    bw_osc_pulse_coeffs _osc_pulse_coeffs {};\n    bw_osc_filt_state   _osc_filt_state {};\n    bw_svf_coeffs       _svf_coeffs {};\n    bw_svf_state        _svf_state {};\n    bw_env_gen_coeffs   _env_gen_coeffs {};\n    bw_env_gen_state    _env_gen_state {};\n\n    RtSafeRtEventFifo _event_fifo;\n    std::array<bool, MAX_MIDI_NOTE> _held_notes {false};\n    int _note {69};\n    char _gate {0};\n    float _freq {440.0f};\n};\n\n} // namespace sushi::internal::simple_synth_plugin\n\nELK_POP_WARNING\n\n#endif //SUSHI_SIMPLE_SYNTH_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/tremolo_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Tremolo from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"tremolo_plugin.h\"\n\nnamespace sushi::internal::tremolo_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.tremolo\";\nconstexpr auto DEFAULT_LABEL = \"Tremolo\";\n\n\nTremoloPlugin::TremoloPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _rate = register_float_parameter(\"rate\", \"Rate\", \"Hz\",\n                                      1.0f, 1.0f, 20.0f,\n                                      Direction::AUTOMATABLE,\n                                      new CubicWarpPreProcessor(1.0f, 20.0f));\n    _amount = register_float_parameter(\"amount\", \"Amount\", \"\",\n                                       1.0f, 0.0f, 1.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    assert(_rate);\n    assert(_amount);\n}\n\nProcessorReturnCode TremoloPlugin::init(float sample_rate)\n{\n    bw_trem_init(&_trem_coeffs);\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid TremoloPlugin::configure(float sample_rate)\n{\n    bw_trem_set_sample_rate(&_trem_coeffs, sample_rate);\n    return;\n}\n\nvoid TremoloPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_trem_reset_coeffs(&_trem_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_trem_reset_state(&_trem_coeffs, &_trem_states[i], 0.0f);\n    }\n}\n\nvoid TremoloPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid TremoloPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid TremoloPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_trem_set_rate(&_trem_coeffs, _rate->processed_value());\n    bw_trem_set_amount(&_trem_coeffs, _amount->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_trem_update_coeffs_ctrl(&_trem_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_trem_update_coeffs_audio(&_trem_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                *out_channel_ptrs[i]++ = bw_trem_process1(&_trem_coeffs, &_trem_states[i],\n                                                            *in_channel_ptrs[i]++);\n            }\n        }\n\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view TremoloPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::tremolo_plugin\n"
  },
  {
    "path": "src/plugins/brickworks/tremolo_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Tremolo from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef TREMOLO_PLUGIN_H\n#define TREMOLO_PLUGIN_H\n\n#include <bw_trem.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::tremolo_plugin {\n\nclass TremoloPlugin : public InternalPlugin, public UidHelper<TremoloPlugin>\n{\npublic:\n    explicit TremoloPlugin(HostControl hostControl);\n\n    ~TremoloPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager{false, std::chrono::milliseconds(30)};\n    float _sample_rate{0};\n\n    FloatParameterValue* _rate;\n    FloatParameterValue* _amount;\n\n    bw_trem_coeffs _trem_coeffs;\n    std::array<bw_trem_state, MAX_TRACK_CHANNELS> _trem_states;\n};\n\n} // namespace sushi::internal::tremolo_plugin\n\nELK_POP_WARNING\n\n#endif // TREMOLO_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/vibrato_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Vibrato from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"vibrato_plugin.h\"\n\nnamespace sushi::internal::vibrato_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.vibrato\";\nconstexpr auto DEFAULT_LABEL = \"Vibrato\";\n\nconstexpr float VIBRATO_AMOUNT_SCALE = 0.0025f;\n\n\nVibratoPlugin::VibratoPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    // The low-level module bw_chorus.h exposes other parameters\n    // (delay & three coefficients for the direct / modulation / feedback branches)\n\n    // but the high-level vibrato example pre-configures them accordingly to Dattoro's reccomendations\n    _rate = register_float_parameter(\"rate\", \"Rate\", \"Hz\",\n                                      4.0f, 2.0f, 10.0f,\n                                      Direction::AUTOMATABLE,\n                                      new CubicWarpPreProcessor(2.0f, 10.0f));\n    _amount = register_float_parameter(\"amount\", \"Amount\", \"\",\n                                       0.0f, 0.0f, 1.0f,\n                                       Direction::AUTOMATABLE,\n                                       new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    assert(_rate);\n    assert(_amount);\n}\n\nProcessorReturnCode VibratoPlugin::init(float sample_rate)\n{\n    // Default values taken from Brickworks example fx_vibrato\n    bw_chorus_init(&_chorus_coeffs, 0.01f);\n    bw_chorus_set_delay(&_chorus_coeffs, 0.0f);\n    bw_chorus_set_coeff_x(&_chorus_coeffs, 0.7071f);\n    bw_chorus_set_coeff_mod(&_chorus_coeffs, 1.f);\n    bw_chorus_set_coeff_fb(&_chorus_coeffs, -0.7071f);\n    configure(sample_rate);\n\n    return ProcessorReturnCode::OK;\n}\n\nvoid VibratoPlugin::configure(float sample_rate)\n{\n    bw_chorus_set_sample_rate(&_chorus_coeffs, sample_rate);\n    return;\n}\n\nvoid VibratoPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_chorus_reset_coeffs(&_chorus_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        _delay_mem_areas[i].resize(bw_chorus_mem_req(&_chorus_coeffs));\n        bw_chorus_mem_set(&_chorus_coeffs, &_chorus_states[i], _delay_mem_areas[i].data());\n    }\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_chorus_reset_state(&_chorus_coeffs, &_chorus_states[i], 0.0f);\n    }\n}\n\nvoid VibratoPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid VibratoPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid VibratoPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_chorus_set_rate(&_chorus_coeffs, _rate->processed_value());\n    float v = _amount->processed_value() * VIBRATO_AMOUNT_SCALE;\n    bw_chorus_set_delay(&_chorus_coeffs, v);\n    bw_chorus_set_amount(&_chorus_coeffs, v);\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_chorus_update_coeffs_ctrl(&_chorus_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_chorus_update_coeffs_audio(&_chorus_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                *out_channel_ptrs[i]++ = bw_chorus_process1(&_chorus_coeffs, &_chorus_states[i],\n                                                            *in_channel_ptrs[i]++);\n            }\n        }\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view VibratoPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::vibrato_plugin\n\n"
  },
  {
    "path": "src/plugins/brickworks/vibrato_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Vibrato from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef VIBRATO_PLUGIN_H\n#define VIBRATO_PLUGIN_H\n\n#include <vector>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_SHORTEN_64_TO_32\nELK_DISABLE_CONVERSION_FROM_SIZE_T_TO_INT\n#include <bw_chorus.h>\nELK_POP_WARNING\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::vibrato_plugin {\n\nclass VibratoPlugin : public InternalPlugin, public UidHelper<VibratoPlugin>\n{\npublic:\n    explicit VibratoPlugin(HostControl hostControl);\n\n    ~VibratoPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager{false, std::chrono::milliseconds(100)};\n    float _sample_rate{0};\n\n    FloatParameterValue* _rate;\n    FloatParameterValue* _amount;\n\n    bw_chorus_coeffs _chorus_coeffs;\n    std::array<bw_chorus_state, MAX_TRACK_CHANNELS> _chorus_states;\n    std::array<std::vector<std::byte>, MAX_TRACK_CHANNELS> _delay_mem_areas;\n};\n\n} // namespace sushi::internal::vibrato_plugin\n\nELK_POP_WARNING\n\n#endif // VIBRATO_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/brickworks/wah_plugin.cpp",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Wah from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"wah_plugin.h\"\n\nnamespace sushi::internal::wah_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.brickworks.wah\";\nconstexpr auto DEFAULT_LABEL = \"Wah\";\n\n\nWahPlugin::WahPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_TRACK_CHANNELS;\n    _max_output_channels = MAX_TRACK_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _wah = register_float_parameter(\"wah\", \"Wah position\", \"\",\n                                    0.5f, 0.0f, 1.0f,\n                                    Direction::AUTOMATABLE,\n                                    new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    assert(_wah);\n}\n\nProcessorReturnCode WahPlugin::init(float sample_rate)\n{\n    bw_wah_init(&_wah_coeffs);\n    bw_wah_set_sample_rate(&_wah_coeffs, sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid WahPlugin::configure(float sample_rate)\n{\n    bw_wah_set_sample_rate(&_wah_coeffs, sample_rate);\n    return;\n}\n\nvoid WahPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    bw_wah_reset_coeffs(&_wah_coeffs);\n    for (int i = 0; i < MAX_TRACK_CHANNELS; i++)\n    {\n        bw_wah_reset_state(&_wah_coeffs, &_wah_states[i], 0.0f);\n    }\n}\n\nvoid WahPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid WahPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        Processor::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid WahPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    bw_wah_set_wah(&_wah_coeffs, _wah->processed_value());\n\n    if (_bypass_manager.should_process())\n    {\n        std::array<const float *, MAX_TRACK_CHANNELS> in_channel_ptrs {};\n        std::array<float *, MAX_TRACK_CHANNELS> out_channel_ptrs {};\n\n        for (int i = 0; i < _current_input_channels; i++)\n        {\n            in_channel_ptrs[i] = in_buffer.channel(i);\n            out_channel_ptrs[i] = out_buffer.channel(i);\n        }\n\n        bw_wah_update_coeffs_ctrl(&_wah_coeffs);\n        for (int n = 0; n < AUDIO_CHUNK_SIZE; n++)\n        {\n            bw_wah_update_coeffs_audio(&_wah_coeffs);\n            for (int i = 0; i < _current_input_channels; i++)\n            {\n                *out_channel_ptrs[i]++ = bw_wah_process1(&_wah_coeffs, &_wah_states[i],\n                                                         *in_channel_ptrs[i]++);\n            }\n        }\n\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view WahPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::wah_plugin\n\n"
  },
  {
    "path": "src/plugins/brickworks/wah_plugin.h",
    "content": "/*\n * Copyright 2017-2025 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Wah from Brickworks library\n * @copyright 2017-2025 Elk Audio AB, Stockholm\n */\n\n#ifndef WAH_PLUGIN_H\n#define WAH_PLUGIN_H\n\n#include <bw_wah.h>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::wah_plugin {\n\nclass WahPlugin : public InternalPlugin, public UidHelper<WahPlugin>\n{\npublic:\n    explicit WahPlugin(HostControl hostControl);\n\n    ~WahPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager{false, std::chrono::milliseconds(30)};\n    float _sample_rate{0};\n\n    FloatParameterValue* _wah;\n\n    bw_wah_coeffs _wah_coeffs;\n    std::array<bw_wah_state, MAX_TRACK_CHANNELS> _wah_states;\n};\n\n} // namespace sushi::internal::wah_plugin\n\nELK_POP_WARNING\n\n#endif // WAH_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/control_to_cv_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Adapter plugin to convert from note on and note\n *        off messages, to cv/gate information, to enable cv/gate control from MIDI plugins.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <algorithm>\n\n#include \"plugins/control_to_cv_plugin.h\"\n\nnamespace sushi::internal::control_to_cv_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.control_to_cv\";\nconstexpr auto DEFAULT_LABEL = \"Keyboard control to CV adapter\";\nconstexpr int TUNE_RANGE = 24;\nconstexpr float PITCH_BEND_RANGE = 12.0f;\nconstexpr int SEND_CHANNEL = 0;\n\nControlToCvPlugin::ControlToCvPlugin(HostControl host_control) :  InternalPlugin(host_control)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _send_velocity_parameter = register_bool_parameter(\"send_velocity\", \"Send Velocity\", \"\", false, Direction::AUTOMATABLE);\n    _send_modulation_parameter = register_bool_parameter(\"send_modulation\", \"Send Modulation\", \"\", false, Direction::AUTOMATABLE);\n    _retrigger_mode_parameter = register_bool_parameter(\"retrigger_enabled\", \"Retrigger enabled\", \"\", false, Direction::AUTOMATABLE);\n\n    _coarse_tune_parameter  = register_int_parameter(\"tune\", \"Tune\", \"semitones\",\n                                                     0, -TUNE_RANGE, TUNE_RANGE,\n                                                     Direction::AUTOMATABLE,\n                                                     new IntParameterPreProcessor(-24, 24));\n\n    _fine_tune_parameter  = register_float_parameter(\"fine_tune\", \"Fine Tune\", \"semitone\",\n                                                     0.0f, -1.0f, 1.0f,\n                                                     Direction::AUTOMATABLE,\n                                                     new FloatParameterPreProcessor(-1, 1));\n\n    _polyphony_parameter  = register_int_parameter(\"polyphony\", \"Polyphony\", \"\",\n                                                   1, 1, MAX_CV_VOICES,\n                                                   Direction::AUTOMATABLE,\n                                                   new IntParameterPreProcessor(1, MAX_CV_VOICES));\n\n    _modulation_parameter  = register_float_parameter(\"modulation\", \"Modulation\", \"\",\n                                                      0.0f, -1.0f, 1.0f,\n                                                      Direction::AUTOMATABLE,\n                                                      new FloatParameterPreProcessor(-1, 1));\n\n    assert(_send_velocity_parameter && _send_modulation_parameter && _coarse_tune_parameter &&\n                                             _polyphony_parameter && _modulation_parameter);\n\n    for (int i = 0; i < MAX_CV_VOICES; ++i)\n    {\n        auto i_str = std::to_string(i);\n        _pitch_parameters[i] = register_float_parameter(\"pitch_\" + i_str, \"Pitch \" + i_str, \"semitones\",\n                                                        0.0f, 0.0f, 1.0f,\n                                                        Direction::AUTOMATABLE,\n                                                        new FloatParameterPreProcessor(0.0f, 1.0f));\n\n        _velocity_parameters[i] = register_float_parameter(\"velocity_\" + i_str, \"Velocity \" + i_str, \"\",\n                                                           0.5f, 0.0f, 1.0f,\n                                                           Direction::AUTOMATABLE,\n                                                           new FloatParameterPreProcessor(0.0f, 1.0f));\n\n        assert(_pitch_parameters[i] && _velocity_parameters[i]);\n    }\n    _max_input_channels = 0;\n    _max_output_channels = 0;\n}\n\nProcessorReturnCode ControlToCvPlugin::init(float sample_rate)\n{\n    return Processor::init(sample_rate);\n}\n\nvoid ControlToCvPlugin::configure(float sample_rate)\n{\n    Processor::configure(sample_rate);\n}\n\nvoid ControlToCvPlugin::process_event(const RtEvent& event)\n{\n    if (is_keyboard_event(event))\n    {\n        _kb_events.push(event);\n        return;\n    }\n    InternalPlugin::process_event(event);\n}\n\nvoid ControlToCvPlugin::process_audio(const ChunkSampleBuffer&  /*in_buffer*/, ChunkSampleBuffer& /*out_buffer*/)\n{\n    if (_bypassed == true)\n    {\n        _kb_events.clear();\n        return;\n    }\n\n    bool send_velocity = _send_velocity_parameter->processed_value();\n    bool send_modulation = _send_modulation_parameter->processed_value();\n    bool retrigger_mode = _retrigger_mode_parameter->processed_value();\n    int coarse_tune = _coarse_tune_parameter->processed_value();\n    float fine_tune = _fine_tune_parameter->processed_value();\n    int polyphony = _polyphony_parameter->processed_value();\n\n    _send_deferred_events();\n    _parse_events(retrigger_mode, polyphony);\n    _send_cv_signals(static_cast<float>(coarse_tune) + fine_tune + _pitch_bend_value, polyphony, send_velocity, send_modulation);\n}\n\nvoid ControlToCvPlugin::_send_deferred_events()\n{\n    while (_deferred_gate_highs.empty() == false)\n    {\n        auto gate_id = _deferred_gate_highs.pop();\n        maybe_output_gate_event(SEND_CHANNEL, gate_id, true);\n    }\n    _deferred_gate_highs.clear();\n}\n\nvoid ControlToCvPlugin::_parse_events(bool retrigger, int polyphony)\n{\n    while (_kb_events.empty() == false)\n    {\n        auto event = _kb_events.pop();\n        switch (event.type())\n        {\n            case RtEventType::NOTE_ON:\n            {\n                auto typed_event = event.keyboard_event();\n                int voice_id = get_free_voice_id(polyphony);\n                auto& voice = _voices[voice_id];\n                if (retrigger && voice.active)\n                {\n                    // Send the gate low event now, and send the gate high event in the next buffer\n                    maybe_output_gate_event(SEND_CHANNEL, voice_id, false);\n                    _deferred_gate_highs.push(voice_id);\n                }\n                else if (voice.active == false)\n                {\n                    maybe_output_gate_event(SEND_CHANNEL, voice_id, true);\n                }\n                voice.active = true;\n                voice.note = typed_event->note();\n                voice.velocity = typed_event->velocity();\n                break;\n            }\n\n            case RtEventType::NOTE_OFF:\n            {\n                auto typed_event = event.keyboard_event();\n                for (int i = 0; i < polyphony; ++i)\n                {\n                    auto& voice = _voices[i];\n                    if (voice.note == typed_event->note() && voice.active)\n                    {\n                        maybe_output_gate_event(SEND_CHANNEL, i, false);\n                        voice.active = false; // TODO - should we handle release velocity, should it be optional?\n                    }\n                }\n                break;\n            }\n\n            case RtEventType::PITCH_BEND:\n            {\n                auto typed_event = event.keyboard_common_event();\n                _pitch_bend_value = typed_event->value() * PITCH_BEND_RANGE;\n                break;\n            }\n            case RtEventType::MODULATION:\n            {\n                auto typed_event = event.keyboard_common_event();\n                _modulation_value = typed_event->value();\n                break;\n            }\n\n            default:\n                break;\n        }\n    }\n    _kb_events.clear();\n}\n\nvoid ControlToCvPlugin::_send_cv_signals(float tune_offset, int polyphony, bool send_velocity, bool send_modulation)\n{\n    // As notes have a non-zero decay, pitch matters even if gate is off, hence always send pitch on all notes\n    for (int i = 0; i < polyphony; ++i)\n    {\n        set_parameter_and_notify(_pitch_parameters[i], pitch_to_cv(static_cast<float>(_voices[i].note) + tune_offset));\n    }\n    if (send_velocity)\n    {\n        for (int i = 0; i < polyphony; ++i)\n        {\n            set_parameter_and_notify(_velocity_parameters[i], _voices[i].velocity);\n        }\n    }\n    if (send_modulation)\n    {\n        set_parameter_and_notify(_modulation_parameter, _modulation_value);\n    }\n}\n\nint ControlToCvPlugin::get_free_voice_id(int polyphony)\n{\n    assert(polyphony <= MAX_CV_VOICES);\n    int voice_id = 0;\n    if (polyphony > 1)\n    {\n        while (voice_id < polyphony && _voices[voice_id].active)\n        {\n            voice_id++;\n        }\n        if (voice_id >= polyphony) // We need to steal an active note, pick the last\n        {\n            voice_id = _last_voice;\n            _last_voice = (_last_voice + 1) % polyphony;\n        }\n        else\n        {\n            _last_voice = 0;\n        }\n    }\n    return voice_id;\n}\n\nstd::string_view ControlToCvPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\nfloat pitch_to_cv(float value)\n{\n    // TODO - this need a lot of tuning, or maybe that tuning should be done someplace else in the code?\n    // Currently just assuming [0, 1] covers a 10 octave linear range.\n    return std::clamp(value / 120.f, 0.0f, 1.0f);\n}\n\n} // end namespace sushi::internal::control_to_cv_plugin\n"
  },
  {
    "path": "src/plugins/control_to_cv_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Adapter plugin to convert cv/gate information to note on and note\n *        off messages to enable cv/gate control of synthesizer plugins.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_CONTROL_TO_CV_PLUGIN_H\n#define SUSHI_CONTROL_TO_CV_PLUGIN_H\n\n#include <array>\n#include <vector>\n\n#include \"sushi/constants.h\"\n\n#include \"library/internal_plugin.h\"\n#include \"library/rt_event_fifo.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::control_to_cv_plugin {\n\nconstexpr int MAX_CV_VOICES = MAX_ENGINE_CV_IO_PORTS;\n\nclass ControlToCvPlugin : public InternalPlugin, public UidHelper<ControlToCvPlugin>\n{\npublic:\n    explicit ControlToCvPlugin(HostControl host_control);\n\n    ~ControlToCvPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer& /*in_buffer*/, ChunkSampleBuffer& /*out_buffer*/) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    struct ControlVoice\n    {\n        bool  active {false};\n        int   note {0};\n        float velocity {0};\n    };\n\n    void _send_deferred_events();\n    void _parse_events(bool retrigger, int polyphony);\n    void _send_cv_signals(float tune_offset, int polyphony, bool send_velocity, bool send_modulation);\n    int get_free_voice_id(int polyphony);\n\n    BoolParameterValue*   _send_velocity_parameter;\n    BoolParameterValue*   _send_modulation_parameter;\n    BoolParameterValue*   _retrigger_mode_parameter;\n    IntParameterValue*    _coarse_tune_parameter;\n    FloatParameterValue*  _fine_tune_parameter;\n    IntParameterValue*    _polyphony_parameter;\n\n    FloatParameterValue* _modulation_parameter;\n    std::array<FloatParameterValue*, MAX_CV_VOICES> _pitch_parameters;\n    std::array<FloatParameterValue*, MAX_CV_VOICES> _velocity_parameters;\n\n    float _pitch_bend_value {0};\n    float _modulation_value {0};\n\n    int                                             _last_voice{0};\n    std::array<ControlVoice, MAX_CV_VOICES>         _voices;\n    RtEventFifo<MAX_ENGINE_GATE_PORTS>              _kb_events;\n    SimpleFifo<int, MAX_ENGINE_GATE_PORTS>          _deferred_gate_highs;\n};\n\nfloat pitch_to_cv(float value);\n\n} // end namespace sushi::internal::control_to_cv_plugin\n\nELK_POP_WARNING\n\n#endif // SUSHI_CONTROL_TO_CV_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/cv_to_control_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Adapter plugin to convert cv/gate information to note on and note\n *        off messages to enable cv/gate control of synthesizer plugins.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <algorithm>\n#include <cmath>\n\n#include \"plugins/cv_to_control_plugin.h\"\n\nnamespace sushi::internal::cv_to_control_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.cv_to_control\";\nconstexpr auto DEFAULT_LABEL = \"Cv to control adapter\";\nconstexpr int TUNE_RANGE = 24;\nconstexpr float PITCH_BEND_RANGE = 12.0f;\n\nCvToControlPlugin::CvToControlPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n    _pitch_bend_mode_parameter = register_bool_parameter(\"pitch_bend_enabled\", \"Pitch bend enabled\", \"\", false, Direction::AUTOMATABLE);\n    _velocity_mode_parameter = register_bool_parameter(\"velocity_enabled\", \"Velocity enabled\", \"\", false, Direction::AUTOMATABLE);\n\n    _channel_parameter  = register_int_parameter(\"channel\", \"Channel\", \"\",\n                                                 0, 0, 16,\n                                                 Direction::AUTOMATABLE,\n                                                 new IntParameterPreProcessor(0, 16));\n\n    _coarse_tune_parameter  = register_int_parameter(\"tune\", \"Tune\", \"semitones\",\n                                                     0, -TUNE_RANGE, TUNE_RANGE,\n                                                     Direction::AUTOMATABLE,\n                                                     new IntParameterPreProcessor(-TUNE_RANGE, TUNE_RANGE));\n\n    _polyphony_parameter  = register_int_parameter(\"polyphony\", \"Polyphony\", \"\",\n                                                   1, 1, MAX_CV_VOICES,\n                                                   Direction::AUTOMATABLE,\n                                                   new IntParameterPreProcessor(1, MAX_CV_VOICES));\n\n    assert(_pitch_bend_mode_parameter && _velocity_mode_parameter && _channel_parameter &&\n                                           _coarse_tune_parameter && _polyphony_parameter);\n\n    for (int i = 0; i < MAX_CV_VOICES; ++i)\n    {\n        auto i_str = std::to_string(i);\n        _pitch_parameters[i] = register_float_parameter(\"pitch_\" + i_str, \"Pitch \" + i_str, \"semitones\",\n                                                        0.0f, 0.0f, 1.0f,\n                                                        Direction::AUTOMATABLE,\n                                                        new FloatParameterPreProcessor(0.0f, 1.0f));\n\n        _velocity_parameters[i] = register_float_parameter(\"velocity_\" + i_str, \"Velocity \" + i_str, \"\",\n                                                           0.5f, 0.0f, 1.0f,\n                                                           Direction::AUTOMATABLE,\n                                                           new FloatParameterPreProcessor(0.0f, 1.0f));\n\n        assert(_pitch_parameters[i] && _velocity_parameters[i]);\n    }\n\n    _max_input_channels = 0;\n    _max_output_channels = 0;\n    _deferred_note_offs.reserve(MAX_ENGINE_CV_IO_PORTS);\n}\n\nProcessorReturnCode CvToControlPlugin::init(float sample_rate)\n{\n    return Processor::init(sample_rate);\n}\n\nvoid CvToControlPlugin::configure(float sample_rate)\n{\n    Processor::configure(sample_rate);\n}\n\nvoid CvToControlPlugin::process_event(const RtEvent& event)\n{\n    // Plugin listens to all channels\n    if (event.type() == RtEventType::NOTE_ON || event.type() == RtEventType::NOTE_OFF)\n    {\n        _gate_events.push(event);\n        return;\n    }\n\n    InternalPlugin::process_event(event);\n}\n\nvoid CvToControlPlugin::process_audio(const ChunkSampleBuffer&  /*in_buffer*/, ChunkSampleBuffer& /*out_buffer*/)\n{\n    if (_bypassed == true)\n    {\n        _gate_events.clear();\n        return;\n    }\n\n    bool send_pitch_bend = _pitch_bend_mode_parameter->processed_value();\n    bool send_velocity = _velocity_mode_parameter->processed_value();\n    int  channel = _channel_parameter->processed_value();\n    int  tune = _coarse_tune_parameter->processed_value();\n    int  polyphony = _polyphony_parameter->processed_value();\n\n    _send_deferred_events(channel);\n    _process_cv_signals(polyphony, channel, tune, send_velocity, send_pitch_bend);\n    _process_gate_changes(polyphony, channel, tune, send_velocity, send_pitch_bend);\n}\n\n\nstd::string_view CvToControlPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\nvoid CvToControlPlugin::_send_deferred_events(int channel)\n{\n    // Note offs that are deferred to create overlapping notes\n    for (auto note : _deferred_note_offs)\n    {\n        output_event(RtEvent::make_note_off_event(0, 0, channel, note, 1.0f));\n    }\n    _deferred_note_offs.clear();\n}\n\nvoid CvToControlPlugin::_process_cv_signals(int polyphony, int channel, int tune, bool send_velocity, bool send_pitch_bend)\n{\n    if (send_pitch_bend && polyphony == 1)\n    {\n        if (_voices[0].active)\n        {\n            /* For now, sending pitch bend only makes sense for monophonic control\n               Eventually add a mode that sends every voice on a separate channel */\n            auto[note, fraction] = cv_to_pitch(_pitch_parameters[0]->processed_value());\n            note += tune;\n            float note_diff = std::clamp((note - _voices[0].note + fraction) / PITCH_BEND_RANGE, -1.0f, 1.0f);\n            output_event(RtEvent::make_pitch_bend_event(0, 0, channel, note_diff));\n        }\n    }\n    else\n    {\n        for (int i = 0; i < polyphony && i < static_cast<int>(_voices.size()); ++i)\n        {\n            auto& voice = _voices[i];\n            if (voice.active)\n            {\n                int new_note;\n                std::tie(new_note, std::ignore) = cv_to_pitch(_pitch_parameters[i]->processed_value());\n                if (voice.note != new_note)\n                {\n                    _deferred_note_offs.push_back(voice.note);\n                    voice.note = new_note;\n                    float velocity = send_velocity ? _velocity_parameters[i]->processed_value() : 1.0f;\n                    output_event(RtEvent::make_note_on_event(0, 0, channel, new_note, velocity));\n                }\n            }\n        }\n    }\n}\n\nvoid CvToControlPlugin::_process_gate_changes(int polyphony, int channel, int tune, bool send_velocity, bool send_pitch_bend)\n{\n    while (_gate_events.empty() == false)\n    {\n        const auto& event = _gate_events.pop();\n        const auto kbd_event = event.keyboard_event();\n        int gate = kbd_event->note();\n        bool gate_state = kbd_event->type() == RtEventType::NOTE_ON;\n        if (gate < polyphony && gate >= 0)\n        {\n            if (gate_state) // Gate high\n            {\n                float velocity = send_velocity ? _velocity_parameters[gate]->processed_value() : 1.0f;\n                _voices[gate].active = true;\n                auto [note, fraction] = cv_to_pitch(_pitch_parameters[gate]->processed_value());\n                note += tune;\n                _voices[gate].note = note;\n                output_event(RtEvent::make_note_on_event(0, 0, channel, note, velocity));\n                if (send_pitch_bend)\n                {\n                    output_event(RtEvent::make_pitch_bend_event(0, 0, channel, fraction / PITCH_BEND_RANGE));\n                }\n            }\n            else // Gate low\n            {\n                _voices[gate].active = false;\n                output_event(RtEvent::make_note_off_event(0, 0, channel, _voices[gate].note, 1.0f));\n            }\n        }\n    }\n}\n\nstd::pair<int, float> cv_to_pitch(float value)\n{\n    // TODO - this need a lot of tuning, or maybe that tuning should be done someplace else in the code?\n    // Currently just assuming [0, 1] covers a 10 octave linear range.\n    double int_note;\n    double fraction = modf(value * 120.0f , &int_note);\n    return {static_cast<int>(int_note), static_cast<float>(fraction)};\n}\n\n} // end namespace sushi::internal::cv_to_control_plugin\n"
  },
  {
    "path": "src/plugins/cv_to_control_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n /**\n  * @brief Adapter plugin to convert cv/gate information to note on and note\n  *        off messages to enable cv/gate control of synthesizer plugins.\n  * @Copyright 2017-2023 Elk Audio AB, Stockholm\n  */\n\n#ifndef SUSHI_CV_TO_CONTROL_PLUGIN_H\n#define SUSHI_CV_TO_CONTROL_PLUGIN_H\n\n#include <array>\n#include <vector>\n\n#include \"sushi/constants.h\"\n\n#include \"library/internal_plugin.h\"\n#include \"library/rt_event_fifo.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::cv_to_control_plugin {\n\nconstexpr int MAX_CV_VOICES = MAX_ENGINE_CV_IO_PORTS;\n\nclass CvToControlPlugin : public InternalPlugin, public UidHelper<CvToControlPlugin>\n{\npublic:\n    explicit CvToControlPlugin(HostControl host_control);\n\n    ~CvToControlPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer& /*in_buffer*/, ChunkSampleBuffer& /*out_buffer*/) override;\n\n    static std::string_view static_uid();\n\nprivate:\n\n    void _send_deferred_events(int channel);\n    void _process_cv_signals(int polyphony, int channel, int tune, bool send_velocity, bool send_pitch_bend);\n    void _process_gate_changes(int polyphony, int channel, int tune, bool send_velocity, bool send_pitch_bend);\n\n    struct ControlVoice\n    {\n        bool active {false};\n        int  note {0};\n    };\n\n    BoolParameterValue* _pitch_bend_mode_parameter;\n    BoolParameterValue* _velocity_mode_parameter;\n    IntParameterValue*  _channel_parameter;\n    IntParameterValue*  _coarse_tune_parameter;\n    IntParameterValue*  _polyphony_parameter;\n\n    std::array<FloatParameterValue*, MAX_CV_VOICES> _pitch_parameters;\n    std::array<FloatParameterValue*, MAX_CV_VOICES> _velocity_parameters;\n\n    std::array<ControlVoice, MAX_CV_VOICES>         _voices;\n    std::vector<int>                                _deferred_note_offs;\n    RtEventFifo<MAX_ENGINE_GATE_PORTS>              _gate_events;\n};\n\nstd::pair<int, float> cv_to_pitch(float value);\n\n} // end namespace sushi::internal::cv_to_control_plugin\n\nELK_POP_WARNING\n\n#endif // SUSHI_CV_TO_CONTROL_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/equalizer_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief 1 band equaliser plugin example\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"equalizer_plugin.h\"\n\nnamespace sushi::internal::equalizer_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.equalizer\";\nconstexpr auto DEFAULT_LABEL = \"Equalizer\";\n\nEqualizerPlugin::EqualizerPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = MAX_CHANNELS_SUPPORTED;\n    _max_output_channels = MAX_CHANNELS_SUPPORTED;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _frequency = register_float_parameter(\"frequency\", \"Frequency\", \"Hz\",\n                                          1000.0f, 20.0f, 20000.0f,\n                                          Direction::AUTOMATABLE,\n                                          new FloatParameterPreProcessor(20.0f, 20000.0f));\n\n    _gain = register_float_parameter(\"gain\", \"Gain\", \"dB\",\n                                     0.0f, -24.0f, 24.0f,\n                                     Direction::AUTOMATABLE,\n                                     new dBToLinPreProcessor(-24.0f, 24.0f));\n\n    _q = register_float_parameter(\"q\", \"Q\", \"\",\n                                  1.0f, 0.0f, 10.0f,\n                                  Direction::AUTOMATABLE,\n                                  new FloatParameterPreProcessor(0.0f, 10.0f));\n    assert(_frequency);\n    assert(_gain);\n    assert(_q);\n}\n\nProcessorReturnCode EqualizerPlugin::init(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    _reset_filters();\n    return ProcessorReturnCode::OK;\n}\n\nvoid EqualizerPlugin::configure(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    _reset_filters();\n    return;\n}\n\nvoid EqualizerPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    _reset_filters();\n}\n\nvoid EqualizerPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    /* Update parameter values */\n    float frequency = _frequency->processed_value();\n    float gain = _gain->processed_value();\n    float q = _q->processed_value();\n\n    if (!_bypassed)\n    {\n        /* Recalculate the coefficients once per audio chunk, this makes for\n         * predictable cpu load for every chunk */\n        dsp::biquad::Coefficients coefficients;\n        dsp::biquad::calc_biquad_peak(coefficients, _sample_rate, frequency, q, gain);\n        for (int i = 0; i < _current_input_channels; ++i)\n        {\n            _filters[i].set_coefficients(coefficients);\n            _filters[i].process(in_buffer.channel(i), out_buffer.channel(i), AUDIO_CHUNK_SIZE);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view EqualizerPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\nvoid EqualizerPlugin::_reset_filters()\n{\n    for (auto& filter : _filters)\n    {\n        filter.set_smoothing(AUDIO_CHUNK_SIZE);\n        filter.reset();\n    }\n}\n\n} // end namespace sushi::internal::equalizer_plugin"
  },
  {
    "path": "src/plugins/equalizer_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief 1 band equaliser plugin example\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef EQUALIZER_PLUGIN_H\n#define EQUALIZER_PLUGIN_H\n\n#include \"library/internal_plugin.h\"\n#include \"dsp_library/biquad_filter.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::equalizer_plugin {\n\nconstexpr int MAX_CHANNELS_SUPPORTED = 2;\n\nclass Accessor;\n\nclass EqualizerPlugin : public InternalPlugin, public UidHelper<EqualizerPlugin>\n{\npublic:\n    explicit EqualizerPlugin(HostControl hostControl);\n\n    ~EqualizerPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    friend Accessor;\n\n    void _reset_filters();\n\n    float _sample_rate;\n    std::array<dsp::biquad::BiquadFilter, MAX_CHANNELS_SUPPORTED> _filters;\n\n    FloatParameterValue* _frequency;\n    FloatParameterValue* _gain;\n    FloatParameterValue* _q;\n};\n\n} // end namespace sushi::internal::equalizer_plugin\n\nELK_POP_WARNING\n\n#endif // EQUALIZER_PLUGIN_H"
  },
  {
    "path": "src/plugins/freeverb_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Wrapper for Freeverb plugin\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n#include <memory>\n\n#include <revmodel.hpp>\n\n#include \"freeverb_plugin.h\"\n\nnamespace sushi::internal::freeverb_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.freeverb\";\nconstexpr auto DEFAULT_LABEL = \"Freeverb\";\n\nFreeverbPlugin::FreeverbPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _max_input_channels = 2;\n    _max_output_channels = 2;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _freeze = register_bool_parameter(\"freeze\", \"Freeze\", \"\",\n                                      false, Direction::AUTOMATABLE);\n    _dry = register_float_parameter(\"dry\", \"Dry Level\", \"\",\n                                    1.0f, 0.0f, 1.0f,\n                                    Direction::AUTOMATABLE,\n                                    new FloatParameterPreProcessor(0.0f, 1.0f));\n    _wet = register_float_parameter(\"wet\", \"Wet Level\", \"\",\n                                    0.5f, 0.0f, 1.0f,\n                                    Direction::AUTOMATABLE,\n                                    new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    _room_size = register_float_parameter(\"room_size\", \"Room Size\", \"\",\n                                          0.5f, 0.0f, 1.0f,\n                                          Direction::AUTOMATABLE,\n                                          new FloatParameterPreProcessor(0.0f, 1.0f));\n    _width = register_float_parameter(\"width\", \"Width\", \"\",\n                                      0.5f, 0.0f, 1.0f,\n                                      Direction::AUTOMATABLE,\n                                      new FloatParameterPreProcessor(0.0f, 1.0f));\n    _damp = register_float_parameter(\"damp\", \"Damping\", \"\",\n                                     0.5f, 0.0f, 1.0f,\n                                     Direction::AUTOMATABLE,\n                                     new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    _reverb_model = std::make_unique<revmodel>();\n\n    assert(_freeze);\n    assert(_dry);\n    assert(_wet);\n    assert(_room_size);\n    assert(_width);\n    assert(_damp);\n}\n\nFreeverbPlugin::~FreeverbPlugin() = default;\n\nProcessorReturnCode FreeverbPlugin::init(float sample_rate)\n{\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid FreeverbPlugin::configure(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    return;\n}\n\nvoid FreeverbPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n}\n\nvoid FreeverbPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid FreeverbPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n    case RtEventType::SET_BYPASS:\n    {\n        bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n        InternalPlugin::set_bypassed(bypassed);\n        _bypass_manager.set_bypass(bypassed, _sample_rate);\n        break;\n    }\n\n    case RtEventType::BOOL_PARAMETER_CHANGE:\n    case RtEventType::FLOAT_PARAMETER_CHANGE:\n    {\n        InternalPlugin::process_event(event);\n        auto typed_event = event.parameter_change_event();\n        if (typed_event->param_id() == _freeze->descriptor()->id())\n        {\n            if (_freeze->processed_value())\n            {\n                _reverb_model->setmode(1.0f);\n            }\n            else\n            {\n                _reverb_model->setmode(0.0f);\n            }\n        }\n        else if (typed_event->param_id() == _dry->descriptor()->id())\n        {\n            _reverb_model->setdry(_dry->processed_value());\n        }\n        else if (typed_event->param_id() == _wet->descriptor()->id())\n        {\n            _reverb_model->setwet(_wet->processed_value());\n        }\n        else if (typed_event->param_id() == _room_size->descriptor()->id())\n        {\n            _reverb_model->setroomsize(_room_size->processed_value());\n        }\n        else if (typed_event->param_id() == _width->descriptor()->id())\n        {\n            _reverb_model->setwidth(_width->processed_value());\n        }\n        else if (typed_event->param_id() == _damp->descriptor()->id())\n        {\n            _reverb_model->setdamp(_damp->processed_value());\n        }\n        break;\n    }\n\n    default:\n        InternalPlugin::process_event(event);\n        break;\n    }\n}\n\nvoid FreeverbPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    if (_bypass_manager.should_process())\n    {\n        float* input_l = const_cast<float*>(in_buffer.channel(0));\n        // reuse the same buffer if we have only one channel\n        float* input_r = const_cast<float*>(in_buffer.channel(0));\n        if (_current_input_channels > 1)\n        {\n            input_r = const_cast<float*>(in_buffer.channel(1));\n        }\n        float* output_l = out_buffer.channel(0);\n        // reuse the same buffer if we have only one channel\n        float* output_r = out_buffer.channel(0);\n        if (_current_output_channels > 1)\n        {\n            output_r = out_buffer.channel(1);\n        }\n\n        _reverb_model->processreplace(input_l, input_r, output_l, output_r, AUDIO_CHUNK_SIZE, 1);\n\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.crossfade_output(in_buffer, out_buffer,\n                                             _current_input_channels,\n                                             _current_output_channels);\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view FreeverbPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n\n} // namespace sushi::internal::freeverb_plugin\n\n"
  },
  {
    "path": "src/plugins/freeverb_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Wrapper for Freeverb plugin\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef FREEVERB_PLUGIN_H\n#define FREEVERB_PLUGIN_H\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nclass revmodel;\n\nnamespace sushi::internal::freeverb_plugin {\n\nclass FreeverbPlugin : public InternalPlugin, public UidHelper<FreeverbPlugin>\n{\npublic:\n    explicit FreeverbPlugin(HostControl hostControl);\n\n    ~FreeverbPlugin() override;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    BypassManager _bypass_manager{false, std::chrono::milliseconds(100)};\n    float _sample_rate{0};\n\n    BoolParameterValue*  _freeze;\n    FloatParameterValue* _dry;\n    FloatParameterValue* _wet;\n    FloatParameterValue* _room_size;\n    FloatParameterValue* _width;\n    FloatParameterValue* _damp;\n\n    std::unique_ptr<revmodel> _reverb_model;\n};\n\n} // namespace sushi::internal::freeverb_plugin\n\nELK_POP_WARNING\n\n#endif // FREEVERB_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/gain_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Gain plugin example\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n\n#include \"gain_plugin.h\"\n\nnamespace sushi::internal::gain_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.gain\";\nconstexpr auto DEFAULT_LABEL = \"Gain\";\n\nGainPlugin::GainPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n    _gain_parameter = register_float_parameter(\"gain\", \"Gain\", \"dB\",\n                                               0.0f, -120.0f, 24.0f,\n                                               Direction::AUTOMATABLE,\n                                               new dBToLinPreProcessor(-120.0f, 24.0f));\n    assert(_gain_parameter);\n}\n\nGainPlugin::~GainPlugin() = default;\n\nvoid GainPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    float gain = _gain_parameter->processed_value();\n    if (!_bypassed)\n    {\n        out_buffer.clear();\n        out_buffer.add_with_gain(in_buffer, gain);\n    } else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nstd::string_view GainPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n} // end namespace sushi::internal::gain_plugin\n"
  },
  {
    "path": "src/plugins/gain_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Gain plugin example\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef GAIN_PLUGIN_H\n#define GAIN_PLUGIN_H\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::gain_plugin {\n\nclass Accessor;\n\nclass GainPlugin : public InternalPlugin, public UidHelper<GainPlugin>\n{\npublic:\n    explicit GainPlugin(HostControl host_control);\n\n    ~GainPlugin() override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    friend Accessor;\n\n    FloatParameterValue* _gain_parameter;\n};\n\n} // end namespace sushi::internal::gain_plugin\n\nELK_POP_WARNING\n\n#endif // GAIN_PLUGIN_H"
  },
  {
    "path": "src/plugins/lfo_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Simple Cv control plugin example\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n#include <numbers>\n\n#include \"lfo_plugin.h\"\n\nnamespace sushi::internal::lfo_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.lfo\";\nconstexpr auto DEFAULT_LABEL = \"Lfo\";\n\nLfoPlugin::LfoPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n    _freq_parameter = register_float_parameter(\"freq\", \"Frequency\", \"Hz\",\n                                               1.0f, 0.001f, 10.0f, Direction::AUTOMATABLE);\n\n    _out_parameter = register_float_parameter(\"out\", \"Lfo Out\", \"\",\n                                              0.5f, 0.0f, 1.0f, Direction::AUTOMATABLE);\n\n    assert(_freq_parameter && _out_parameter);\n}\n\nLfoPlugin::~LfoPlugin() = default;\n\nProcessorReturnCode LfoPlugin::init(float sample_rate)\n{\n    _buffers_per_second = sample_rate / AUDIO_CHUNK_SIZE;\n    return ProcessorReturnCode::OK;\n}\n\nvoid LfoPlugin::configure(float sample_rate)\n{\n    _buffers_per_second = sample_rate / AUDIO_CHUNK_SIZE;\n}\n\nvoid LfoPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    bypass_process(in_buffer, out_buffer);\n    _phase += static_cast<float>(_freq_parameter->processed_value() * std::numbers::pi_v<float> / _buffers_per_second);\n    this->set_parameter_and_notify(_out_parameter, (std::sin(_phase) + 1) * 0.5f);\n}\n\nstd::string_view LfoPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n} // end namespace sushi::internal::lfo_plugin\n"
  },
  {
    "path": "src/plugins/lfo_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Simple Cv control plugin example\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef LFO_PLUGIN_H\n#define LFO_PLUGIN_H\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::lfo_plugin {\n\nclass LfoPlugin : public InternalPlugin, public UidHelper<LfoPlugin>\n{\npublic:\n    explicit LfoPlugin(HostControl host_control);\n\n    ~LfoPlugin() override;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    float _phase {0};\n    float _buffers_per_second {0};\n    FloatParameterValue* _freq_parameter;\n    FloatParameterValue* _out_parameter;\n};\n\n} // end namespace sushi::internal::lfo_plugin\n\nELK_POP_WARNING\n\n#endif // LFO_PLUGIN_H"
  },
  {
    "path": "src/plugins/mono_summing_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Plugin to sum all input channels and output the result to all output channels\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"mono_summing_plugin.h\"\n\nnamespace sushi::internal::mono_summing_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.mono_summing\";\nconstexpr auto DEFAULT_LABEL = \"Mono summing\";\n\nMonoSummingPlugin::MonoSummingPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n}\n\nMonoSummingPlugin::~MonoSummingPlugin() = default;\n\nvoid MonoSummingPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    if (_bypassed == false)\n    {\n        for (int output_channel = 0; output_channel < out_buffer.channel_count(); ++output_channel)\n        {\n            int input_channel = 0;\n            out_buffer.replace(output_channel, input_channel, in_buffer);\n            for (input_channel = 1; input_channel < in_buffer.channel_count(); ++input_channel)\n            {\n                out_buffer.add(output_channel, input_channel, in_buffer);\n            }\n        }\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n\n}\n\nstd::string_view MonoSummingPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n} // end namespace sushi::internal::mono_summing_plugin\n\n"
  },
  {
    "path": "src/plugins/mono_summing_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Plugin to sum all input channels and output the result to all output channels\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_MONO_SUMMING_PLUGIN_H\n#define SUSHI_MONO_SUMMING_PLUGIN_H\n\n#include \"library/internal_plugin.h\"\n#include \"library/rt_event_fifo.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::mono_summing_plugin {\n\nclass MonoSummingPlugin : public InternalPlugin, public UidHelper<MonoSummingPlugin>\n{\npublic:\n    explicit MonoSummingPlugin(HostControl host_control);\n\n    ~MonoSummingPlugin() override;\n\n    void process_event(const RtEvent& event) override\n    {\n        InternalPlugin::process_event(event);\n    };\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n};\n\n} // end namespace sushi::internal::mono_summing_plugin\n\nELK_POP_WARNING\n\n#endif // SUSHI_MONO_SUMMING_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/passthrough_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Example unit gain plugin\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"passthrough_plugin.h\"\n\nnamespace sushi::internal::passthrough_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.passthrough\";\nconstexpr auto DEFAULT_LABEL = \"Passthrough\";\n\nPassthroughPlugin::PassthroughPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n}\n\nPassthroughPlugin::~PassthroughPlugin() = default;\n\nvoid PassthroughPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    bypass_process(in_buffer, out_buffer);\n}\n\nstd::string_view PassthroughPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n} // end namespace sushi::internal::passthrough_plugin\n"
  },
  {
    "path": "src/plugins/passthrough_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Example unit gain plugin. Passes audio unprocessed from input to output\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef PASSTHROUGH_PLUGIN_H\n#define PASSTHROUGH_PLUGIN_H\n\n#include \"library/internal_plugin.h\"\n#include \"library/rt_event_fifo.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::passthrough_plugin {\n\nclass PassthroughPlugin : public InternalPlugin, public UidHelper<PassthroughPlugin>\n{\npublic:\n    explicit PassthroughPlugin(HostControl host_control);\n\n    ~PassthroughPlugin() override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n};\n\n} // end namespace sushi::internal::passthrough_plugin\n\nELK_POP_WARNING\n\n#endif // PASSTHROUGH_PLUGIN_H\n\n"
  },
  {
    "path": "src/plugins/peak_meter_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Audio processor with event output example\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n#include \"spdlog/fmt/bundled/format.h\"\n\n#include \"peak_meter_plugin.h\"\n\nnamespace sushi::internal::peak_meter_plugin {\n\nconstexpr float DEFAULT_REFRESH_RATE = 25;\n// The time for meters to drop ~10dB\nconstexpr auto REFRESH_TIME = std::chrono::milliseconds(250);\n\nconstexpr auto CLIP_HOLD_TIME = std::chrono::seconds(5);\n\n// Full range of the output parameters is -120dB to +24dB\nconstexpr float OUTPUT_MIN_DB = -120.0f;\nconstexpr float OUTPUT_MAX_DB = 24.0f;\nconstexpr float OUTPUT_MIN = 1.0e-6f; // -120dB\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.peakmeter\";\nconstexpr auto DEFAULT_LABEL = \"Peak Meter\";\n\n// Convert a gain value to a normalised gain value\ninline float to_normalised_dB(float gain)\n{\n    float db_gain = 20.0f * std::log10(std::max(gain, OUTPUT_MIN));\n    float norm = (db_gain - OUTPUT_MIN_DB) / (OUTPUT_MAX_DB - OUTPUT_MIN_DB);\n    return std::clamp(norm, 0.0f, 1.0f);\n}\n\nPeakMeterPlugin::PeakMeterPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _clip_hold_count.fill(0);\n    _clipped.fill(false);\n    _max_input_channels = MAX_METERED_CHANNELS;\n    _max_output_channels = MAX_METERED_CHANNELS;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _link_channels_parameter = register_bool_parameter(\"link_channels\", \"Link Channels 1 & 2\", \"\", false, Direction::AUTOMATABLE);\n    _send_peaks_only_parameter = register_bool_parameter(\"peaks_only\", \"Peaks Only\", \"\", false, Direction::AUTOMATABLE);\n    _update_rate_parameter = register_float_parameter(\"update_rate\", \"Update Rate\", \"/s\", DEFAULT_REFRESH_RATE,\n                                                      0.1f, 25,\n                                                      Direction::AUTOMATABLE,\n                                                      new FloatParameterPreProcessor(0.1f, DEFAULT_REFRESH_RATE));\n    _update_rate_id = _update_rate_parameter->descriptor()->id();\n\n    std::string_view param_name = \"level_{}\";\n    std::string_view param_label = \"Level ch {}\";\n    for (int i = 0; i < MAX_METERED_CHANNELS; ++i)\n    {\n        _level_parameters[i] = register_float_parameter(fmt::vformat(param_name, fmt::make_format_args(i)),\n                                                        fmt::vformat(param_label, fmt::make_format_args(i)),\n                                                        \"dB\",\n                                                        OUTPUT_MIN_DB, OUTPUT_MIN_DB, OUTPUT_MAX_DB,\n                                                        Direction::OUTPUT,\n                                                        new dBToLinPreProcessor(OUTPUT_MIN_DB, OUTPUT_MAX_DB));\n        assert (_level_parameters[i]);\n    }\n\n    param_name = \"clip_{}\";\n    param_label = \"Clip ch {}\";\n    for (int i = 0; i < MAX_METERED_CHANNELS; ++i)\n    {\n        _clip_parameters[i] = register_bool_parameter(fmt::vformat(param_name, fmt::make_format_args(i)),\n                                                      fmt::vformat(param_label, fmt::make_format_args(i)),\n                                                      \"\",\n                                                      false,\n                                                      Direction::OUTPUT);\n    }\n\n    assert(_link_channels_parameter && _send_peaks_only_parameter && _update_rate_parameter);\n}\n\nvoid PeakMeterPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    bypass_process(in_buffer, out_buffer);\n\n    bool linked = _link_channels_parameter->processed_value();\n    bool send_only_peaks = _send_peaks_only_parameter->processed_value();\n    _process_peak_detection(in_buffer, linked, send_only_peaks);\n    _process_clip_detection(in_buffer, linked);\n}\n\nProcessorReturnCode PeakMeterPlugin::init(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    _update_refresh_interval(DEFAULT_REFRESH_RATE, sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid PeakMeterPlugin::configure(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    _update_refresh_interval(_update_rate_parameter->processed_value(), sample_rate);\n}\n\nvoid PeakMeterPlugin::process_event(const RtEvent& event)\n{\n    InternalPlugin::process_event(event);\n\n    if (event.type() == RtEventType::FLOAT_PARAMETER_CHANGE &&\n        event.parameter_change_event()->param_id() == _update_rate_id)\n    {\n        _update_refresh_interval(_update_rate_parameter->processed_value(), _sample_rate);\n    }\n}\n\nvoid PeakMeterPlugin::_update_refresh_interval(float rate, float sample_rate)\n{\n    _refresh_interval = static_cast<int>(std::round(sample_rate / rate));\n    _clip_hold_samples = static_cast<uint64_t>(sample_rate * CLIP_HOLD_TIME.count());\n    for (auto& i :  _smoothers)\n    {\n        i.set_lag_time(REFRESH_TIME, sample_rate / AUDIO_CHUNK_SIZE);\n    }\n}\n\nvoid PeakMeterPlugin::_process_peak_detection(const ChunkSampleBuffer& in, bool linked, bool send_only_peaks)\n{\n    std::array<float, MAX_METERED_CHANNELS> peak;\n    peak.fill(0.0f);\n\n    int channels = std::min(MAX_METERED_CHANNELS, in.channel_count());\n\n    for (int ch = 0; ch < channels; ++ch)\n    {\n        peak[ch] = in.calc_peak_value(ch);\n    }\n\n    if (linked && in.channel_count() > 1)\n    {\n        float max_peak = std::max(peak[0], peak[1]);\n        peak[0] = max_peak;\n        peak[1] = max_peak;\n    }\n\n    bool update = false;\n    _sample_count += AUDIO_CHUNK_SIZE;\n    if (_sample_count > _refresh_interval)\n    {\n        _sample_count -= _refresh_interval;\n        update = true;\n        if (send_only_peaks)\n        {\n            update = _peak_hysteresis;\n        }\n    }\n\n    for (int ch = 0; ch < channels; ++ch)\n    {\n        float value = peak[ch];\n        auto& filter = _smoothers[ch];\n        if (value > filter.value())\n        {\n            filter.set_direct(value);\n            _peak_hysteresis = true;\n        }\n        else\n        {\n            filter.set(value);\n        }\n\n        if (update)\n        {\n            set_parameter_and_notify(_level_parameters[ch], to_normalised_dB(filter.value()));\n            _peak_hysteresis = false;\n        }\n        filter.next_value();\n    }\n}\n\nvoid PeakMeterPlugin::_process_clip_detection(const ChunkSampleBuffer& in, bool linked)\n{\n    std::array<bool, MAX_METERED_CHANNELS> clipped_ch;\n    clipped_ch.fill(false);\n\n    int channels = std::min(MAX_METERED_CHANNELS, in.channel_count());\n\n    for (int ch = 0; ch < channels; ++ch)\n    {\n        clipped_ch[ch] = in.count_clipped_samples(ch) > 0;\n    }\n\n    if (linked && channels > 1)\n    {\n        clipped_ch[0] |= clipped_ch[1];\n        clipped_ch[1] |= clipped_ch[0];\n    }\n\n    for (int ch = 0; ch < std::min(MAX_METERED_CHANNELS, in.channel_count()); ++ch)\n    {\n        if (clipped_ch[ch])\n        {\n            _clip_hold_count[ch] = 0;\n            if (_clipped[ch] == false)\n            {\n                _clipped[ch] = true;\n                set_parameter_and_notify(_clip_parameters[ch], true);\n            }\n        }\n        else if (_clipped[ch] && _clip_hold_count[ch] > _clip_hold_samples)\n        {\n            _clipped[ch] = false;\n            set_parameter_and_notify(_clip_parameters[ch], false);\n        }\n        _clip_hold_count[ch] += AUDIO_CHUNK_SIZE;\n    }\n}\n\nstd::string_view PeakMeterPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n} // end namespace sushi::internal::gain_plugin\n"
  },
  {
    "path": "src/plugins/peak_meter_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Audio processor with event output example\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_PEAK_METER_PLUGIN_H\n#define SUSHI_PEAK_METER_PLUGIN_H\n\n#include \"library/internal_plugin.h\"\n#include \"dsp_library/value_smoother.h\"\n\n#include \"engine/track.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::peak_meter_plugin {\n\nconstexpr int MAX_METERED_CHANNELS = MAX_TRACK_CHANNELS;\n\nclass PeakMeterPlugin : public InternalPlugin, public UidHelper<PeakMeterPlugin>\n{\npublic:\n    explicit PeakMeterPlugin(HostControl host_control);\n\n    ~PeakMeterPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    void _process_peak_detection(const ChunkSampleBuffer& in, bool linked, bool send_only_peaks);\n\n    void _process_clip_detection(const ChunkSampleBuffer& in, bool linked);\n\n    void _update_refresh_interval(float rate, float sample_rate);\n\n    // Output parameters\n    std::array<FloatParameterValue*, MAX_METERED_CHANNELS> _level_parameters;\n    std::array<BoolParameterValue*, MAX_METERED_CHANNELS> _clip_parameters;\n\n    // Input parameters\n    BoolParameterValue*  _link_channels_parameter;\n    BoolParameterValue*  _send_peaks_only_parameter;\n    FloatParameterValue* _update_rate_parameter;\n    ObjectId             _update_rate_id;\n\n    uint64_t _clip_hold_samples;\n    std::array<uint64_t, MAX_METERED_CHANNELS> _clip_hold_count;\n    std::array<bool, MAX_METERED_CHANNELS> _clipped;\n\n    int _refresh_interval;\n    int _sample_count {0};\n    bool _peak_hysteresis {true};\n\n    float _sample_rate;\n\n    std::array<ValueSmootherFilter<float>, MAX_METERED_CHANNELS> _smoothers;\n};\n\n} // end namespace sushi::internal::peak_meter_plugin\n\nELK_POP_WARNING\n\n#endif // SUSHI_PEAK_METER_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/return_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Aux return plugin to return audio from a send plugin\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <algorithm>\n\n#include \"elklog/static_logger.h\"\n\n#include \"return_plugin.h\"\n#include \"send_plugin.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"return_plugin\");\n\nnamespace sushi::internal::return_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.return\";\nconstexpr auto DEFAULT_LABEL = \"Return\";\n\nReturnPlugin::ReturnPlugin(HostControl host_control, SendReturnFactory* manager) : InternalPlugin(host_control),\n                                                                                   _manager(manager)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n    _max_input_channels = MAX_SEND_CHANNELS;\n    _max_output_channels = MAX_SEND_CHANNELS;\n}\n\nReturnPlugin::~ReturnPlugin()\n{\n    _manager->on_return_destruction(this);\n    for (auto& sender : _senders)\n    {\n        sender->clear_destination();\n    }\n}\n\nvoid ReturnPlugin::send_audio(const ChunkSampleBuffer& buffer, int start_channel, float gain, int thread_id)\n{\n    std::scoped_lock<SpinLock> lock(_buffer_lock);\n    _maybe_swap_buffers(_host_control.transport()->current_samples());\n\n    /* If the sender invoking this function is on the same thread, and process_audio() has not yet been called, we can\n     * copy directly to _active_out (i.e. zero delay), if not, we must copy to _active_in (with 1 buffer delay) */\n    auto target_buffer = (thread_id == _current_processing_thread && !_processed_this_block)? _active_out : _active_in;\n\n    int max_channels = std::max(0, std::min(buffer.channel_count(), _current_output_channels - start_channel));\n\n    for (int c = 0 ; c < max_channels; ++c)\n    {\n        target_buffer->add_with_gain(start_channel++, c, buffer, gain);\n    }\n}\n\nvoid ReturnPlugin::send_audio_with_ramp(const ChunkSampleBuffer& buffer, int start_channel,\n                                        float start_gain, float end_gain, int thread_id)\n{\n    std::scoped_lock<SpinLock> lock(_buffer_lock);\n    _maybe_swap_buffers(_host_control.transport()->current_samples());\n\n    /* See comment in send_audio() */\n    auto target_buffer = (thread_id == _current_processing_thread && !_processed_this_block)? _active_out : _active_in;\n\n    int max_channels = std::max(0, std::min(buffer.channel_count(), _current_output_channels - start_channel));\n\n    for (int c = 0 ; c < max_channels; ++c)\n    {\n        target_buffer->add_with_ramp(start_channel++, c, buffer, start_gain, end_gain);\n    }\n}\n\nvoid ReturnPlugin::add_sender(send_plugin::SendPlugin* sender)\n{\n    _senders.push_back(sender);\n}\n\nvoid ReturnPlugin::remove_sender(send_plugin::SendPlugin* sender)\n{\n    _senders.erase(std::remove(_senders.begin(), _senders.end(), sender), _senders.end());\n}\n\nProcessorReturnCode ReturnPlugin::init(float sample_rate)\n{\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid ReturnPlugin::configure(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    for (auto& buffer : _buffers)\n    {\n        buffer.clear();\n    }\n}\n\nvoid ReturnPlugin::set_channels(int inputs, int outputs)\n{\n    Processor::set_channels(inputs, outputs);\n\n    int max_channels = std::max(inputs, outputs);\n    if (_buffers.front().channel_count() != max_channels)\n    {\n        _buffers.fill(ChunkSampleBuffer(max_channels));\n    }\n}\n\nvoid ReturnPlugin::set_enabled(bool enabled)\n{\n    if (enabled == false)\n    {\n        for (auto& buffer : _buffers)\n        {\n            buffer.clear();\n        }\n    }\n}\n\nvoid ReturnPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n        case RtEventType::SET_BYPASS:\n        {\n            bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n            Processor::set_bypassed(bypassed);\n            _bypass_manager.set_bypass(bypassed, _sample_rate);\n            break;\n        }\n        default:\n            InternalPlugin::process_event(event);\n            break;\n    }\n}\n\nvoid ReturnPlugin::process_audio(const ChunkSampleBuffer& /*in_buffer*/, ChunkSampleBuffer& out_buffer)\n{\n    {\n        std::scoped_lock<SpinLock> lock(_buffer_lock);\n        _maybe_swap_buffers(_host_control.transport()->current_samples());\n    }\n\n    if (_bypass_manager.should_process())\n    {\n        auto buffer = ChunkSampleBuffer::create_non_owning_buffer(*_active_out, 0, out_buffer.channel_count());\n        out_buffer.replace(buffer);\n\n        if (_bypass_manager.should_ramp())\n        {\n            _bypass_manager.ramp_output(out_buffer);\n        }\n    }\n    else\n    {\n        out_buffer.clear();\n    }\n    _processed_this_block = true;\n}\n\nbool ReturnPlugin::bypassed() const\n{\n    return _bypass_manager.bypassed();\n}\n\nvoid ReturnPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nstd::string_view ReturnPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\nvoid inline ReturnPlugin::_swap_buffers()\n{\n    std::swap(_active_in, _active_out);\n    _active_in->clear();\n}\n\nvoid inline ReturnPlugin::_maybe_swap_buffers(int64_t current_samples)\n{\n    int64_t prev_samples = _last_process_samples.load(std::memory_order_acquire);\n    if (prev_samples != current_samples)\n    {\n        _last_process_samples.store(current_samples, std::memory_order_release);\n        _processed_this_block = false;\n        _swap_buffers();\n    }\n}\n\n} // end namespace sushi::internal::return_plugin"
  },
  {
    "path": "src/plugins/return_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Aux return plugin to return audio from a Send Plugin\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_RETURN_PLUGIN_H\n#define SUSHI_RETURN_PLUGIN_H\n\n#include <atomic>\n\n#include \"library/spinlock.h\"\n#include \"send_return_factory.h\"\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal {\n\n// Forward declarations to keep the number of includes down\nclass SendReturnFactory;\n\nnamespace send_plugin { class SendPlugin; }\n\nconstexpr int THREAD_ID_UNKNOWN = -1;\n\nnamespace return_plugin {\n\nclass Accessor;\n\nclass ReturnPlugin : public InternalPlugin, public UidHelper<ReturnPlugin>\n{\npublic:\n    ReturnPlugin(HostControl host_control, SendReturnFactory* manager);\n\n    ~ReturnPlugin() override;\n\n    void send_audio(const ChunkSampleBuffer& buffer, int start_channel, float gain, int thread_id);\n\n    void send_audio_with_ramp(const ChunkSampleBuffer& buffer, int start_channel, float start_gain, float end_gain, int thread_id);\n\n    void add_sender(send_plugin::SendPlugin* sender);\n\n    void remove_sender(send_plugin::SendPlugin* sender);\n\n    // From Processor\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_channels(int inputs, int outputs) override;\n\n    void set_enabled(bool enabled) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer) override;\n\n    bool bypassed() const override;\n\n    void set_bypassed(bool bypassed) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    friend Accessor;\n\n    void inline _swap_buffers();\n\n    void inline _maybe_swap_buffers(int64_t current_samples);\n\n    float                                 _sample_rate;\n    SendReturnFactory*                    _manager;\n\n    std::array<ChunkSampleBuffer, 2>      _buffers;\n    ChunkSampleBuffer*                    _active_in{&_buffers[0]};\n    ChunkSampleBuffer*                    _active_out{&_buffers[1]};\n    bool                                  _processed_this_block{false};\n\n    SpinLock                              _buffer_lock;\n\n    std::vector<send_plugin::SendPlugin*> _senders;\n\n    BypassManager                         _bypass_manager;\n\n    std::atomic<Time>                     _last_process_time{Time(0)};\n    std::atomic<int64_t>                  _last_process_samples{0};\n\n    static_assert(decltype(_last_process_time)::is_always_lock_free);\n};\n\n} // end namespace return_plugin\n\n} // end namespace sushi::internal\n\nELK_POP_WARNING\n\n#endif // SUSHI_RETURN_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/sample_delay_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Audio processor with event output example\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"sample_delay_plugin.h\"\n\nnamespace sushi::internal::sample_delay_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.sample_delay\";\nconstexpr auto DEFAULT_LABEL = \"Sample delay\";\n\nSampleDelayPlugin::SampleDelayPlugin(HostControl host_control) : InternalPlugin(host_control),\n                                                                 _write_idx(0),\n                                                                 _read_idx(0)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n    _sample_delay = register_int_parameter(\"sample_delay\", \n                                           \"Sample delay\", \n                                           \"samples\", \n                                           0,\n                                           0,\n                                           MAX_DELAY - 1,\n                                           Direction::AUTOMATABLE);\n    for (int i = 0; i < DEFAULT_CHANNELS; i++)\n    {\n        _delaylines.push_back(std::array<float, MAX_DELAY>());\n    }\n}\n\nvoid SampleDelayPlugin::set_channels(int inputs, int outputs)\n{\n    Processor::set_channels(inputs, 0);\n\n    int max_channels = std::max(inputs, outputs);\n    if (_delaylines.size() != static_cast<size_t>(max_channels))\n    {\n        _delaylines.resize(max_channels);\n        _reset();\n    }\n}\n\nvoid SampleDelayPlugin::process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer)\n{\n    // update delay value\n    _read_idx = (_write_idx + MAX_DELAY - _sample_delay->processed_value()) % MAX_DELAY;\n\n    // process\n    if (_bypassed == false)\n    {\n        int n_channels = std::min(in_buffer.channel_count(), out_buffer.channel_count());\n        for (int channel_idx = 0; channel_idx < n_channels; channel_idx++)\n        {\n            int temp_write_idx = _write_idx;\n            int temp_read_idx = _read_idx;\n            for (int sample_idx = 0; sample_idx < AUDIO_CHUNK_SIZE; sample_idx++)\n            {\n                _delaylines[channel_idx][temp_write_idx] = in_buffer.channel(channel_idx)[sample_idx];\n                out_buffer.channel(channel_idx)[sample_idx] = _delaylines[channel_idx][temp_read_idx];\n                temp_write_idx++;\n                temp_read_idx++;\n                temp_write_idx %= MAX_DELAY;\n                temp_read_idx %= MAX_DELAY;\n            }\n        }\n        _write_idx += AUDIO_CHUNK_SIZE;\n        _read_idx += AUDIO_CHUNK_SIZE;\n        _write_idx %= MAX_DELAY;\n        _read_idx %= MAX_DELAY;\n    }\n    else\n    {\n        bypass_process(in_buffer, out_buffer);\n    }\n}\n\nvoid SampleDelayPlugin::set_enabled(bool enabled)\n{\n    if (enabled == false)\n    {\n        _reset();\n    }\n}\n\nstd::string_view SampleDelayPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\nvoid SampleDelayPlugin::_reset()\n{\n    for (auto& line : _delaylines)\n    {\n        line.fill(0.0f);\n    }\n    _read_idx = 0;\n    _write_idx = 0;\n}\n\n} // end namespace sushi::internal::sample_delay_plugin\n"
  },
  {
    "path": "src/plugins/sample_delay_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Audio processor with event output example\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SAMPLE_DELAY_PLUGIN_H\n#define SUSHI_SAMPLE_DELAY_PLUGIN_H\n\n#include <array>\n#include <memory>\n#include <vector>\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::sample_delay_plugin {\n\nconstexpr int MAX_DELAY = 48000;\n\nclass SampleDelayPlugin : public InternalPlugin, public UidHelper<SampleDelayPlugin>\n{\npublic:\n    explicit SampleDelayPlugin(HostControl host_control);\n\n    ~SampleDelayPlugin() override = default;\n\n    void set_channels(int inputs, int outputs) override;\n    \n    void set_enabled(bool enabled) override;\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    void _reset();\n\n    // Input parameters\n    IntParameterValue* _sample_delay;\n    \n    // Delayline data\n    int _write_idx;\n    int _read_idx;\n    std::vector<std::array<float, MAX_DELAY>> _delaylines;\n};\n\n} // end namespace sushi::internal::sample_delay_plugin\n\nELK_POP_WARNING\n\n#endif // !SUSHI_SAMPLE_DELAY_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/sample_player_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Sampler plugin example to test event and sample handling\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n#include <sndfile.h>\n\n#include \"elklog/static_logger.h\"\n\n#include \"sample_player_plugin.h\"\n\nnamespace sushi::internal::sample_player_plugin {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"sampleplayer\");\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.sampleplayer\";\nconstexpr auto DEFAULT_LABEL = \"Sample player\";\nconstexpr int SAMPLE_PROPERTY_ID = 0;\n\nBlobData load_sample_file(const std::string& file_name)\n{\n    SNDFILE* sample_file;\n    SF_INFO  soundfile_info = {};\n\n    sample_file = sf_open(file_name.c_str(), SFM_READ, &soundfile_info);\n\n    if (!sample_file)\n    {\n        ELKLOG_LOG_ERROR(\"Failed to open sample file: {}\", file_name);\n        return {0, nullptr};\n    }\n\n    sf_count_t samples = 0;\n    float* sample_buffer = new float[soundfile_info.frames];\n    if (soundfile_info.channels == 1)\n    {\n        samples = sf_readf_float(sample_file, sample_buffer, soundfile_info.frames);\n    }\n    else // Decode interleaved stereo\n    {\n        float buffer[2];\n        for (int i = 0; i < soundfile_info.frames; ++i)\n        {\n            samples += sf_readf_float(sample_file, buffer, 1);\n            sample_buffer[i] = buffer[0];\n        }\n    }\n\n    sf_close(sample_file);\n\n    if (samples <= 0)\n    {\n        delete[] sample_buffer;\n        return {0, nullptr};\n    }\n\n    return BlobData{static_cast<int>(soundfile_info.frames * sizeof(float)), reinterpret_cast<uint8_t*>(sample_buffer)};\n}\n\nSamplePlayerPlugin::SamplePlayerPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n    [[maybe_unused]] bool str_pr_ok = register_property(\"sample_file\", \"Sample File\", \"\");\n\n    _volume_parameter  = register_float_parameter(\"volume\", \"Volume\", \"dB\",\n                                                  0.0f, -120.0f, 36.0f,\n                                                  Direction::AUTOMATABLE,\n                                                  new dBToLinPreProcessor(-120.0f, 36.0f));\n\n    _attack_parameter  = register_float_parameter(\"attack\", \"Attack\", \"s\",\n                                                  0.0f, 0.0f, 10.0f,\n                                                  Direction::AUTOMATABLE,\n                                                  new FloatParameterPreProcessor(0.0f, 10.0f));\n\n    _decay_parameter   = register_float_parameter(\"decay\", \"Decay\", \"s\",\n                                                  0.0f, 0.0f, 10.0f,\n                                                  Direction::AUTOMATABLE,\n                                                  new FloatParameterPreProcessor(0.0f, 10.0f));\n\n    _sustain_parameter = register_float_parameter(\"sustain\", \"Sustain\", \"\",\n                                                  1.0f, 0.0f, 1.0f,\n                                                  Direction::AUTOMATABLE,\n                                                  new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    _release_parameter = register_float_parameter(\"release\", \"Release\", \"s\",\n                                                  0.0f, 0.0f, 10.0f,\n                                                  Direction::AUTOMATABLE,\n                                                  new FloatParameterPreProcessor(0.0f, 10.0f));\n\n    assert(_volume_parameter && _attack_parameter && _decay_parameter && _sustain_parameter && _release_parameter && str_pr_ok);\n    _max_input_channels = 0;\n}\n\nProcessorReturnCode SamplePlayerPlugin::init(float sample_rate)\n{\n    _sample.set_sample(&_dummy_sample, 0);\n    for (auto& voice : _voices)\n    {\n        voice.set_samplerate(sample_rate);\n        voice.set_sample(&_sample);\n    }\n\n    return ProcessorReturnCode::OK;\n}\n\nvoid SamplePlayerPlugin::configure(float sample_rate)\n{\n    for (auto& voice : _voices)\n    {\n        voice.set_samplerate(sample_rate);\n    }\n}\n\nvoid SamplePlayerPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n    if (enabled == false)\n    {\n        _all_notes_off();\n    }\n}\n\nvoid SamplePlayerPlugin::set_bypassed(bool bypassed)\n{\n    // Kill all voices when bypassed, so we don't have any hanging notes when turning back on\n    if (bypassed)\n    {\n        _all_notes_off();\n    }\n    Processor::set_bypassed(bypassed);\n}\n\nSamplePlayerPlugin::~SamplePlayerPlugin()\n{\n    delete[] _sample_buffer;\n}\n\nvoid SamplePlayerPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n        case RtEventType::NOTE_ON:\n        {\n            if (_bypassed)\n            {\n                break;\n            }\n            bool voice_allocated = false;\n            auto key_event = event.keyboard_event();\n            ELKLOG_LOG_DEBUG(\"Sample Player: note ON, num. {}, vel. {}\",\n                            key_event->note(), key_event->velocity());\n            for (auto& voice : _voices)\n            {\n                if (!voice.active())\n                {\n                    voice.note_on(key_event->note(), key_event->velocity(), event.sample_offset());\n                    voice_allocated = true;\n                    break;\n                }\n            }\n            // TODO - improve voice stealing algorithm\n            if (!voice_allocated)\n            {\n                for (auto& voice : _voices)\n                {\n                    if (voice.stopping())\n                    {\n                        voice.note_on(key_event->note(), key_event->velocity(), event.sample_offset());\n                        break;\n                    }\n                }\n            }\n            break;\n        }\n        case RtEventType::NOTE_OFF:\n        {\n            if (_bypassed)\n            {\n                break;\n            }\n            auto key_event = event.keyboard_event();\n            ELKLOG_LOG_DEBUG(\"Sample Player: note OFF, num. {}, vel. {}\",\n                            key_event->note(), key_event->velocity());\n            for (auto& voice : _voices)\n            {\n                if (voice.active() && voice.current_note() == key_event->note())\n                {\n                    voice.note_off(key_event->velocity(), event.sample_offset());\n                    break;\n                }\n            }\n            break;\n        }\n\n        case RtEventType::NOTE_AFTERTOUCH:\n        case RtEventType::PITCH_BEND:\n        case RtEventType::AFTERTOUCH:\n        case RtEventType::MODULATION:\n        case RtEventType::WRAPPED_MIDI_EVENT:\n            // Consume these events so they are not propagated\n            break;\n\n        case RtEventType::DATA_PROPERTY_CHANGE:\n        {\n            // Kill all voices before swapping out the sample\n            for (auto& voice : _voices)\n            {\n                voice.note_off(1.0f, 0);\n            }\n\n            auto typed_event = event.data_parameter_change_event();\n            auto new_sample = typed_event->value();\n            float* old_sample = _sample_buffer;\n            _sample_buffer = reinterpret_cast<float*>(new_sample.data);\n            _sample.set_sample(_sample_buffer, static_cast<int>(new_sample.size / sizeof(float)));\n\n            // Delete the old sample data outside the rt thread\n            BlobData data{0, reinterpret_cast<uint8_t*>(old_sample)};\n            auto delete_event = RtEvent::make_delete_blob_event(data);\n            output_event(delete_event);\n            break;\n        }\n\n        default:\n            InternalPlugin::process_event(event);\n            break;\n    }\n}\n\n\nvoid SamplePlayerPlugin::process_audio(const ChunkSampleBuffer& /* in_buffer */, ChunkSampleBuffer& out_buffer)\n{\n    float gain = _volume_parameter->processed_value();\n    float attack = _attack_parameter->processed_value();\n    float decay = _decay_parameter->processed_value();\n    float sustain = _sustain_parameter->processed_value();\n    float release = _release_parameter->processed_value();\n\n    _buffer.clear();\n    out_buffer.clear();\n    for (auto& voice : _voices)\n    {\n        voice.set_envelope(attack, decay, sustain, release);\n        voice.render(_buffer);\n    }\n    if (!_bypassed)\n    {\n        out_buffer.add_with_gain(_buffer, gain);\n    }\n}\n\nProcessorReturnCode SamplePlayerPlugin::set_property_value(ObjectId property_id, const std::string& value)\n{\n    if (property_id == SAMPLE_PROPERTY_ID)\n    {\n        auto sample_data = load_sample_file(value);\n        if (sample_data.size > 0)\n        {\n            send_data_to_realtime(sample_data, 0);\n        }\n    }\n    return InternalPlugin::set_property_value(property_id, value);\n}\n\nstd::string_view SamplePlayerPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\nvoid SamplePlayerPlugin::_all_notes_off()\n{\n    for (auto &voice : _voices)\n    {\n        voice.note_off(1.0f, 0);\n    }\n}\n\n} // end namespace sushi::internal::sample_player_plugin\n"
  },
  {
    "path": "src/plugins/sample_player_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Sampler plugin example to test event and sample handling\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SAMPLER_PLUGIN_H\n#define SUSHI_SAMPLER_PLUGIN_H\n\n#include <array>\n\n#include \"library/internal_plugin.h\"\n#include \"plugins/sample_player_voice.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::sample_player_plugin {\n\nconstexpr size_t TOTAL_POLYPHONY = 8;\n\nclass Accessor;\n\nclass SamplePlayerPlugin : public InternalPlugin, public UidHelper<SamplePlayerPlugin>\n{\npublic:\n    explicit SamplePlayerPlugin(HostControl host_control);\n\n    ~SamplePlayerPlugin() override;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override ;\n\n    void process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer) override;\n\n    ProcessorReturnCode set_property_value(ObjectId property_id, const std::string& value) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    friend Accessor;\n\n    void _all_notes_off();\n\n    float*  _sample_buffer {nullptr};\n    float   _dummy_sample {0.0f};\n    dsp::Sample _sample;\n\n    SampleBuffer<AUDIO_CHUNK_SIZE> _buffer {1};\n    FloatParameterValue* _volume_parameter;\n    FloatParameterValue* _attack_parameter;\n    FloatParameterValue* _decay_parameter;\n    FloatParameterValue* _sustain_parameter;\n    FloatParameterValue* _release_parameter;\n\n    std::array<sample_player_voice::Voice, TOTAL_POLYPHONY> _voices;\n};\n\n} // end namespace sushi::internal::sample_player_plugin\n\nELK_POP_WARNING\n\n#endif // SUSHI_SAMPLER_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/sample_player_voice.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Voice class for sample player plugin\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cassert>\n#include <cmath>\n\n#include \"plugins/sample_player_voice.h\"\n\nnamespace sushi::internal::sample_player_voice {\n\n/* Using a method inspired by a vst example to track notes. The voice\n * itself has no event queue so only one note on and one note off\n * event can be triggered per audio chunk. This puts a limit on the\n * number of notes per second an instrument can play, which is the\n * number of audio chunks per second * the polyphony.\n * For 6 voices and 64 sample chunks this amounts to a maximum of 4000\n * notes per second. Which should be still be enough for all but the\n * most absurd Black Midi compositions.\n */\nvoid Voice::note_on(int note, float velocity, int offset)\n{\n    offset = std::min(offset, AUDIO_CHUNK_SIZE - 1);\n\n    /* Completely ignore any currently playing note, it will be cut off abruptly */\n    _state = SamplePlayMode::STARTING;\n    _velocity_gain = velocity;\n    _start_offset = offset;\n    _stop_offset = AUDIO_CHUNK_SIZE;\n    _playback_pos = 0.0;\n    _current_note = note;\n    /* The root note of the sample is assumed to be C4 in 44100 Hz*/\n    _playback_speed = powf(2, (note - 60)/12.0f) * _samplerate / SAMPLE_FILE_RATE;\n    _envelope.gate(true);\n}\n\n/* Release velocity is ignored atm. Has any synth ever supported it? */\nvoid Voice::note_off(float /*velocity*/, int offset)\n{\n    assert(offset < AUDIO_CHUNK_SIZE);\n    if (_state == SamplePlayMode::PLAYING || _state == SamplePlayMode::STARTING)\n    {\n        _state = SamplePlayMode::STOPPING;\n        _stop_offset = offset;\n    }\n}\n\nvoid Voice::reset()\n{\n    _state = SamplePlayMode::STOPPED;\n    _envelope.reset();\n}\n\nvoid Voice::render(sushi::SampleBuffer<AUDIO_CHUNK_SIZE>& output_buffer)\n{\n    if (_state == SamplePlayMode::STOPPED)\n    {\n        return;\n    }\n    /* Handle only mono samples for now */\n    float* out = output_buffer.channel(0);\n\n    for (int i = _start_offset; i < _stop_offset; ++i)\n    {\n        out[i] += _sample->at(_playback_pos) * _velocity_gain * _envelope.tick(1);\n        _playback_pos += _playback_speed;\n    }\n\n    /* If there is a note off event, set the envelope to off and\n     * render the rest of the chunk */\n    if (_state == SamplePlayMode::STOPPING)\n    {\n        _envelope.gate(false);\n        for (int i = _stop_offset; i < AUDIO_CHUNK_SIZE; ++i)\n        {\n            out[i] += _sample->at(_playback_pos) * _velocity_gain * _envelope.tick(1);\n            _playback_pos += _playback_speed;\n        }\n    }\n\n    /* Handle state changes and reset render limits */\n    switch (_state)\n    {\n        case SamplePlayMode::STARTING:\n            _state = SamplePlayMode::PLAYING;\n            _start_offset = 0;\n            break;\n\n        case SamplePlayMode::STOPPING:\n            if (_envelope.finished())\n            {\n                _state = SamplePlayMode::STOPPED;\n            }\n            break;\n\n        default:\n            break;\n    }\n}\n\n} // end namespace sushi::internal::sample_player_voice"
  },
  {
    "path": "src/plugins/sample_player_voice.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Voice class for sample player plugin\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SAMPLE_VOICE_H\n#define SUSHI_SAMPLE_VOICE_H\n\n#include \"sushi/sample_buffer.h\"\n\n#include \"dsp_library/envelopes.h\"\n#include \"dsp_library/sample_wrapper.h\"\n\nnamespace sushi::internal::sample_player_voice {\n\n// TODO eventually make this configurable\nconstexpr float SAMPLE_FILE_RATE = 44100.0f;\n\nenum class SamplePlayMode\n{\n    STOPPED,\n    STARTING,\n    PLAYING,\n    STOPPING\n};\n\nclass Voice\n{\n    SUSHI_DECLARE_NON_COPYABLE(Voice);\npublic:\n    Voice() = default;\n\n    Voice(float samplerate, dsp::Sample* sample) : _samplerate(samplerate), _sample(sample) {}\n\n    /**\n     * @brief Runtime samplerate configuration.\n     * @param samplerate The playback samplerate.\n     */\n    void set_samplerate(float samplerate)\n    {\n        _playback_speed = _playback_speed * samplerate / _samplerate;\n        _envelope.set_samplerate(samplerate / AUDIO_CHUNK_SIZE);\n        _samplerate = samplerate;\n    }\n\n    /**\n     * @brief Runtime sample configuration\n     * @param sample\n     */\n    void set_sample(dsp::Sample* sample) {_sample = sample;}\n\n    /**\n     * @brief Set the envelope parameters.\n     */\n    void set_envelope(float attack, float decay, float sustain, float release)\n    {\n        _envelope.set_parameters(attack, decay, sustain, release);\n    }\n\n    /**\n     * @brief Is currently playing sound.\n     * @return True if currently playing sound.\n     */\n    bool active() {return (_state != SamplePlayMode::STOPPED);}\n\n    /**\n     * @brief Is currently in the release phase but still playing.\n     * @return True if note is currently off but still sounding.\n     */\n    bool stopping() {return _state == SamplePlayMode::STOPPING;}\n\n    /**\n     * @brief Return the current note being played, if any.\n     * @return The current note as a midi note number.\n     */\n    int current_note() {return _current_note;}\n\n    /**\n     * @brief Play a new note within this audio chunk\n     * @param note The midi note number to play, with 60 as middle C.\n     * @param velocity Velocity of the note to play. 0 to 1.\n     * @param time_offset Offset in samples from the start of the chunk.\n     */\n    void note_on(int note, float velocity, int offset);\n\n    /**\n     * @brief Stop the currently playing note.\n     * @param velocity Release velocity, not used, only included for completeness.\n     * @param time_offset Offset in samples from start of current audio chunk.\n     */\n    void note_off(float velocity, int offset);\n\n    /**\n     * @brief Reset the voice and kill all sound.\n     */\n    void reset();\n\n    /**\n     * @brief Render one chunk of audio.\n     * @param output_buffer Target buffer.\n     */\n    void render(sushi::SampleBuffer<AUDIO_CHUNK_SIZE>& output_buffer);\n\nprivate:\n    float _samplerate{44100};\n    dsp::Sample* _sample;\n    SamplePlayMode _state{SamplePlayMode::STOPPED};\n    dsp::AdsrEnvelope _envelope;\n    int _current_note;\n    float _playback_speed;\n    float _velocity_gain;\n    double _playback_pos{0.0};\n    int _start_offset{0};\n    int _stop_offset{0};\n};\n\n} // end namespace sushi::internal::sample_player_voice\n\n#endif // SUSHI_SAMPLE_VOICE_H\n"
  },
  {
    "path": "src/plugins/send_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Aux send plugin to send audio to a return plugin\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <string>\n\n#include \"elklog/static_logger.h\"\n\n#include \"send_plugin.h\"\n\n#include \"sushi/constants.h\"\n\n#include \"return_plugin.h\"\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"send_plugin\");\n\nnamespace sushi::internal::send_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.send\";\nconstexpr auto DEFAULT_LABEL = \"Send\";\nconstexpr auto DEFAULT_DEST = \"No destination\";\nconstexpr auto DEST_PROPERTY_ID = 4;\n\nSendPlugin::SendPlugin(HostControl host_control, SendReturnFactory* manager) : InternalPlugin(host_control),\n                                                                               _manager(manager)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n    _gain_parameter  = register_float_parameter(\"gain\", \"Gain\", \"dB\",\n                                                0.0f, -120.0f, 24.0f,\n                                                Direction::AUTOMATABLE,\n                                                new dBToLinPreProcessor(-120.0f, 24.0f));\n\n    _channel_count_parameter  = register_int_parameter(\"channel_count\", \"Channel count\", \"\",\n                                                       MAX_TRACK_CHANNELS, 0, MAX_TRACK_CHANNELS,\n                                                       Direction::AUTOMATABLE);\n    _start_channel_parameter  = register_int_parameter(\"start_channel\", \"Start channel\", \"\",\n                                                       0, 0, MAX_TRACK_CHANNELS,\n                                                       Direction::AUTOMATABLE);\n    _dest_channel_parameter  = register_int_parameter(\"dest_channel\", \"Destination channel\", \"\",\n                                                       0, 0, MAX_TRACK_CHANNELS,\n                                                       Direction::AUTOMATABLE);\n\n    _gain_smoother.set_direct(_gain_parameter->processed_value());\n\n    [[maybe_unused]] bool str_pr_ok = register_property(\"destination_name\", \"destination name\", DEFAULT_DEST);\n    assert(_gain_parameter && str_pr_ok);\n    assert(_channel_count_parameter && _start_channel_parameter && _dest_channel_parameter);\n}\n\nSendPlugin::~SendPlugin()\n{\n    if (_destination)\n    {\n        _destination->remove_sender(this);\n    }\n}\n\nvoid SendPlugin::clear_destination()\n{\n    _destination = nullptr;\n    set_property_value(DEST_PROPERTY_ID, DEFAULT_DEST);\n}\n\nvoid SendPlugin::_set_destination(return_plugin::ReturnPlugin* destination)\n{\n    assert(destination);\n\n    if (_destination)\n    {\n        _destination->remove_sender(this);\n    }\n\n    _destination = destination;\n    destination->add_sender(this);\n}\n\nProcessorReturnCode SendPlugin::init(float sample_rate)\n{\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid SendPlugin::configure(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    _gain_smoother.set_lag_time(GAIN_SMOOTHING_TIME, sample_rate / AUDIO_CHUNK_SIZE);\n}\n\nvoid SendPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n        case RtEventType::SET_BYPASS:\n        {\n            bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n            Processor::set_bypassed(bypassed);\n            _bypass_manager.set_bypass(bypassed, _sample_rate);\n            break;\n        }\n\n        default:\n            InternalPlugin::process_event(event);\n            break;\n    }\n}\n\nvoid SendPlugin::process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer)\n{\n    bypass_process(in_buffer, out_buffer);\n\n    if (_bypass_manager.should_process() && _destination)\n    {\n        float gain = _gain_parameter->processed_value();\n        _gain_smoother.set(gain);\n\n        int dest_channel = _dest_channel_parameter->processed_value();\n        int start_channel = _start_channel_parameter->processed_value();\n        int channels = std::min(_channel_count_parameter->processed_value(), in_buffer.channel_count() - start_channel);\n\n        if (channels <= 0)\n        {\n            return;\n        }\n\n        const auto buffer = ChunkSampleBuffer::create_non_owning_buffer(const_cast<ChunkSampleBuffer&>(in_buffer),\n                                                                        start_channel, channels);\n\n        // Ramp if bypass was recently toggled\n        if (_bypass_manager.should_ramp())\n        {\n            auto [start, end] = _bypass_manager.get_ramp();\n            start *= _gain_smoother.value();\n            end *= _gain_smoother.next_value();\n            _destination->send_audio_with_ramp(buffer, dest_channel, start, end, _current_processing_thread);\n        }\n\n        // Don't ramp, nominal case\n        else if (_gain_smoother.stationary())\n        {\n            _destination->send_audio(buffer, dest_channel, gain, _current_processing_thread);\n        }\n\n        // Ramp because send gain was recently changed\n        else\n        {\n            float start = _gain_smoother.value();\n            float end = _gain_smoother.next_value();\n            _destination->send_audio_with_ramp(buffer, dest_channel, start, end, _current_processing_thread);\n        }\n    }\n}\n\nbool SendPlugin::bypassed() const\n{\n    return _bypass_manager.bypassed();\n}\n\nvoid SendPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nProcessorReturnCode SendPlugin::set_property_value(ObjectId property_id, const std::string& value)\n{\n    if (property_id == DEST_PROPERTY_ID)\n    {\n        _change_return_destination(value);\n    }\n    return InternalPlugin::set_property_value(property_id, value);\n}\n\nstd::string_view SendPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\nvoid SendPlugin::_change_return_destination(const std::string& dest_name)\n{\n    return_plugin::ReturnPlugin* return_plugin = _manager->lookup_return_plugin(dest_name);\n    if (return_plugin)\n    {\n        _set_destination(return_plugin);\n    }\n    else\n    {\n        ELKLOG_LOG_WARNING(\"Return plugin {} not found\", dest_name);\n    }\n}\n\n} // end namespace sushi::internal::send_plugin\n"
  },
  {
    "path": "src/plugins/send_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Aux send plugin to send audio to a return plugin\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SEND_PLUGIN_H\n#define SUSHI_SEND_PLUGIN_H\n\n#include <atomic>\n\n#include \"sushi/constants.h\"\n\n#include \"send_return_factory.h\"\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal {\n\nclass SendReturnFactory;\n\nnamespace return_plugin { class ReturnPlugin; }\n\nconstexpr int MAX_SEND_CHANNELS = MAX_TRACK_CHANNELS;\n\nnamespace send_plugin {\n\nclass Accessor;\n\nclass SendPlugin : public InternalPlugin, public UidHelper<SendPlugin>\n{\npublic:\n    SendPlugin(HostControl host_control, SendReturnFactory* manager);\n\n    ~SendPlugin() override;\n\n    void clear_destination();\n\n    // From Processor\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer) override;\n\n    bool bypassed() const override;\n\n    void set_bypassed(bool bypassed) override;\n\n    ProcessorReturnCode set_property_value(ObjectId property_id, const std::string& value) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    friend Accessor;\n\n    void _set_destination(return_plugin::ReturnPlugin* destination);\n\n    void _change_return_destination(const std::string& dest_name);\n\n    float                         _sample_rate;\n    return_plugin::ReturnPlugin*  _destination {nullptr};\n\n    FloatParameterValue*          _gain_parameter;\n    ValueSmootherFilter<float>    _gain_smoother;\n\n    IntParameterValue*            _channel_count_parameter;\n    IntParameterValue*            _start_channel_parameter;\n    IntParameterValue*            _dest_channel_parameter;\n\n    SendReturnFactory* _manager;\n    BypassManager _bypass_manager;\n};\n\n} // end namespace sushi::internal\n} // end namespace send_plugin\n\nELK_POP_WARNING\n\n#endif // SUSHI_SEND_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/send_return_factory.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Factory class to create send and return plugins and manage the shared resources\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"send_return_factory.h\"\n#include \"send_plugin.h\"\n#include \"return_plugin.h\"\n\nnamespace sushi::internal {\n\nSendReturnFactory::SendReturnFactory() = default;\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"send_ret_factory\");\n\nsend_plugin::SendPlugin* SendReturnFactory::get_send()\n{\n    return nullptr;\n}\n\nreturn_plugin::ReturnPlugin* SendReturnFactory::lookup_return_plugin(const std::string& name)\n{\n    return_plugin::ReturnPlugin* instance = nullptr;\n    std::scoped_lock<std::mutex> lock(_return_inst_lock);\n    for (const auto i : _return_instances)\n    {\n        if (i->name() == name)\n        {\n            instance = i;\n            break;\n        }\n    }\n    ELKLOG_LOG_INFO(\"Looked up return plugin {}, {}\", name, instance? \"found\" : \"not found\");\n    return instance;\n}\n\nvoid SendReturnFactory::on_return_destruction(return_plugin::ReturnPlugin* instance)\n{\n    std::scoped_lock<std::mutex> lock(_return_inst_lock);\n    _return_instances.erase(std::remove(_return_instances.begin(), _return_instances.end(), instance), _return_instances.end());\n}\n\nstd::pair<ProcessorReturnCode, std::shared_ptr<Processor>>\nSendReturnFactory::new_instance(const PluginInfo& plugin_info, HostControl& host_control, float sample_rate)\n{\n    std::shared_ptr<Processor> processor;\n\n    if (plugin_info.uid == send_plugin::SendPlugin::static_uid())\n    {\n        processor = std::make_shared<send_plugin::SendPlugin>(host_control, this);\n    }\n    else if (plugin_info.uid == return_plugin::ReturnPlugin::static_uid())\n    {\n        auto instance = std::make_shared<return_plugin::ReturnPlugin>(host_control, this);\n        std::scoped_lock<std::mutex> lock(_return_inst_lock);\n        _return_instances.push_back(instance.get());\n        processor = instance;\n    }\n\n    if (processor)\n    {\n        auto processor_status = processor->init(sample_rate);\n        return {processor_status, processor};\n    }\n    return {ProcessorReturnCode::ERROR, processor};\n}\n\n} // end namespace sushi::internal\n"
  },
  {
    "path": "src/plugins/send_return_factory.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Factory class to create send and return plugins and manage the shared resources\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_SEND_RETURN_FACTORY_H\n#define SUSHI_SEND_RETURN_FACTORY_H\n\n#include <mutex>\n\n#include \"library/base_processor_factory.h\"\n#include \"send_plugin.h\"\n#include \"return_plugin.h\"\n\nnamespace sushi::internal {\n\nclass SendReturnFactory : public BaseProcessorFactory\n{\npublic:\n    SendReturnFactory();\n\n    ~SendReturnFactory() override = default;\n\n    send_plugin::SendPlugin* get_send();\n\n    return_plugin::ReturnPlugin* lookup_return_plugin(const std::string& name);\n\n    void on_return_destruction(return_plugin::ReturnPlugin* instance);\n\n    // From BaseProcessorFactory\n    std::pair<ProcessorReturnCode, std::shared_ptr<Processor>> new_instance(const PluginInfo &plugin_info,\n                                                                            HostControl& host_control,\n                                                                            float sample_rate) override;\n\nprivate:\n    std::vector<return_plugin::ReturnPlugin*> _return_instances;\n    std::mutex _return_inst_lock;\n};\n\n} // end namespace sushi::internal\n\n#endif // SUSHI_SEND_RETURN_FACTORY_H\n"
  },
  {
    "path": "src/plugins/step_sequencer_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Simple 8-step sequencer example.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <algorithm>\n\n#include \"plugins/step_sequencer_plugin.h\"\n\nnamespace sushi::internal::step_sequencer_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.step_sequencer\";\nconstexpr auto DEFAULT_LABEL = \"Step Sequencer\";\n\nconstexpr float SECONDS_IN_MINUTE = 60.0f;\nconstexpr float MULTIPLIER_8TH_NOTE = 2.0f;\nconstexpr int   OCTAVE = 12;\nconstexpr std::array<int, 12> MINOR_SCALE = {0,0,2,3,3,5,5,7,8,8,10,10};\n\nStepSequencerPlugin::StepSequencerPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n    for (int i = 0; i < SEQUENCER_STEPS; ++i)\n    {\n        std::string str_nr = std::to_string(i);\n        _pitch_parameters[i] = register_int_parameter(\"pitch_\" + str_nr, \"Pitch \" + str_nr, \"semitone\",\n                                                      0, -24, 24,\n                                                      Direction::AUTOMATABLE,\n                                                      new IntParameterPreProcessor(-24, 24));\n\n        _step_parameters[i] = register_bool_parameter(\"step_\" + str_nr, \"Step \" + str_nr, \"\",\n                                                      true, Direction::AUTOMATABLE);\n\n        _step_indicator_parameters[i] = register_bool_parameter(\"step_ind_\" + str_nr, \"Step Indication \" + str_nr, \"\",\n                                                                true, Direction::AUTOMATABLE);\n\n        assert(_pitch_parameters[i] && _step_indicator_parameters[i] && _step_indicator_parameters[i]);\n    }\n    for (auto& s : _sequence)\n    {\n        s = START_NOTE;\n    }\n}\n\nProcessorReturnCode StepSequencerPlugin::init(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    return ProcessorReturnCode::OK;\n}\n\nvoid StepSequencerPlugin::configure(float sample_rate)\n{\n    _sample_rate = sample_rate;\n}\n\nvoid StepSequencerPlugin::set_bypassed(bool bypassed)\n{\n    Processor::set_bypassed(bypassed);\n}\n\nvoid StepSequencerPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n        case RtEventType::NOTE_ON:\n        {\n            auto typed_event = event.keyboard_event();\n            _transpose = typed_event->note() - START_NOTE;\n            _event_queue.push(event);\n            break;\n        }\n\n        case RtEventType::NOTE_OFF:\n        case RtEventType::MODULATION:\n        case RtEventType::PITCH_BEND:\n        case RtEventType::AFTERTOUCH:\n        case RtEventType::WRAPPED_MIDI_EVENT:\n        {\n            _event_queue.push(event);\n            break;\n        }\n        case RtEventType::FLOAT_PARAMETER_CHANGE:\n        case RtEventType::INT_PARAMETER_CHANGE:\n        case RtEventType::BOOL_PARAMETER_CHANGE:\n        {\n            auto typed_event = event.parameter_change_event();\n            /* Ugly way of identifying a step parameter change and outputing an indicator param update*/\n            if (typed_event->param_id() % 3 == 1)\n            {\n                set_parameter_and_notify(_step_indicator_parameters[typed_event->param_id()/3], typed_event->value() > 0.5f);\n            }\n            [[fallthrough]];\n        }\n        default:\n            InternalPlugin::process_event(event);\n    }\n}\n\nvoid StepSequencerPlugin::process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer)\n{\n    bypass_process(in_buffer, out_buffer);\n    if (_host_control.transport()->playing_mode() == PlayingMode::STOPPED)\n    {\n        // If not playing, we pass keyboard events through unchanged.\n        while (_event_queue.empty() == false)\n        {\n            output_event(_event_queue.pop());\n        }\n        // If stopping, we kill the current note playing.\n        if (_host_control.transport()->current_state_change() == PlayStateChange::STOPPING)\n        {\n            output_event(RtEvent::make_note_off_event(this->id(), 0, 0, _current_note, 1.0f));\n        }\n        return;\n    }\n\n    float start_beat = static_cast<float>(_host_control.transport()->current_bar_beats() * MULTIPLIER_8TH_NOTE);\n    float end_beat = static_cast<float>(_host_control.transport()->current_bar_beats(AUDIO_CHUNK_SIZE) * MULTIPLIER_8TH_NOTE);\n\n    /* New 8th note during this chunk */\n    if (static_cast<int>(end_beat) - static_cast<int>(start_beat) != 0)\n    {\n        int step = static_cast<int>(end_beat);\n        if (step >= SEQUENCER_STEPS)\n        {\n            return;\n        }\n        int offset = static_cast<int>((end_beat - std::floor(end_beat)) /\n                samples_per_qn(_host_control.transport()->current_tempo(), _sample_rate));\n        if (_current_step_active)\n        {\n            RtEvent note_off = RtEvent::make_note_off_event(this->id(), offset, 0, _current_note, 1.0f);\n            output_event(note_off);\n        }\n\n        /* Indicator for the current step is turned off if the current step is active and\n         * vice versa in order to provide visual feedback when the sequencer is running */\n        set_parameter_and_notify(_step_indicator_parameters[_current_step], _current_step_active);\n        _current_step = step;\n        _current_step_active = _step_parameters[step]->processed_value();\n        set_parameter_and_notify(_step_indicator_parameters[step], !_current_step_active);\n\n        if (_current_step_active)\n        {\n            _current_note = snap_to_scale(_pitch_parameters[step]->processed_value() + START_NOTE, MINOR_SCALE) + _transpose;\n            RtEvent note_on = RtEvent::make_note_on_event(this->id(), offset, 0, _current_note, 1.0f);\n            output_event(note_on);\n        }\n    }\n\n    _event_queue.clear();\n}\n\nstd::string_view StepSequencerPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\nfloat samples_per_qn(float tempo, float samplerate)\n{\n    return 8.0f * samplerate / tempo * SECONDS_IN_MINUTE;\n}\n\nint snap_to_scale(int note, const std::array<int, 12>& scale)\n{\n    int octave = note / OCTAVE;\n    return scale[note % OCTAVE] + octave * OCTAVE;\n}\n\n} // end namespace sushi::internal::step_sequencer_plugin\n"
  },
  {
    "path": "src/plugins/step_sequencer_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Simple 8-step sequencer example.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_STEP_SEQUENCER_PLUGIN_H\n#define SUSHI_STEP_SEQUENCER_PLUGIN_H\n\n#include <array>\n\n#include \"library/internal_plugin.h\"\n#include \"library/rt_event_fifo.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::step_sequencer_plugin {\n\nconstexpr int SEQUENCER_STEPS = 8;\nconstexpr int START_NOTE = 48;\nconstexpr int NOTE_EVENT_QUEUE_SIZE = 40;\n\nclass StepSequencerPlugin : public InternalPlugin, public UidHelper<StepSequencerPlugin>\n{\npublic:\n    explicit StepSequencerPlugin(HostControl host_control);\n\n    ~StepSequencerPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    std::array<IntParameterValue*, SEQUENCER_STEPS>  _pitch_parameters;\n    std::array<BoolParameterValue*, SEQUENCER_STEPS> _step_parameters;\n    std::array<BoolParameterValue*, SEQUENCER_STEPS> _step_indicator_parameters;\n    std::array<int, SEQUENCER_STEPS> _sequence;\n\n    float   _sample_rate;\n    int     _current_step {0};\n    bool    _current_step_active {true};\n    int     _transpose {0};\n    int     _current_note {0};\n\n    RtEventFifo<NOTE_EVENT_QUEUE_SIZE> _event_queue;\n};\n\nfloat samples_per_qn(float tempo, float samplerate);\nint snap_to_scale(int note, const std::array<int, 12>& scale);\n\n} // end namespace sushi::internal::step_sequencer_plugin\n\nELK_POP_WARNING\n\n#endif // SUSHI_STEP_SEQUENCER_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/stereo_mixer_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Stereo mixer\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"stereo_mixer_plugin.h\"\n\n#include \"sushi/constants.h\"\n\nnamespace sushi::internal::stereo_mixer_plugin {\n\nconstexpr char PLUGIN_UID[] = \"sushi.testing.stereo_mixer\";\nconstexpr char DEFAULT_LABEL[] = \"Stereo Mixer\";\n\nconstexpr int MAX_CHANNELS_SUPPORTED = 2;\n\n/**\n * @brief Panning calculation using the same law as for the tracks but scaled\n * to keep the gain constant in the default \"passthrough\" behaviour.\n *\n * @param gain The gain to apply the pan to\n * @param pan The pan amount\n * @return std::pair<float, float> the gain for the <right, left> channel\n */\ninline std::pair<float, float> calc_l_r_gain(float gain, float pan)\n{\n    float left_gain, right_gain;\n    if (pan < 0.0f) // Audio panned left\n    {\n        left_gain = gain * (1.0f + pan - PAN_GAIN_3_DB * pan);\n        right_gain = gain * (1.0f + pan);\n    }\n    else            // Audio panned right\n    {\n        left_gain = gain * (1.0f - pan);\n        right_gain = gain * (1.0f - pan + PAN_GAIN_3_DB * pan);\n    }\n    return {left_gain / PAN_GAIN_3_DB, right_gain / PAN_GAIN_3_DB};\n}\n\nStereoMixerPlugin::StereoMixerPlugin(HostControl HostControl): InternalPlugin(HostControl)\n{\n    _max_input_channels = MAX_CHANNELS_SUPPORTED;\n    _max_output_channels = MAX_CHANNELS_SUPPORTED;\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _ch1_pan = register_float_parameter(\"ch1_pan\", \"Channel 1 Pan\", \"\",\n                                        -1.0, -1.0, 1.0,\n                                        Direction::AUTOMATABLE,\n                                        nullptr);\n\n    _ch1_gain = register_float_parameter(\"ch1_gain\", \"Channel 1 Gain\", \"\",\n                                         0.0f, -120.0f, 24.0f,\n                                         Direction::AUTOMATABLE,\n                                         new dBToLinPreProcessor(-120.0f, 24.0));\n\n    _ch1_invert_phase = register_float_parameter(\"ch1_invert_phase\", \"Channel 1 Invert Phase\", \"\",\n                                                 0.0f, 0.0f, 1.0f,\n                                                 Direction::AUTOMATABLE,\n                                                 nullptr);\n    _ch2_pan = register_float_parameter(\"ch2_pan\", \"Channel 2 Pan\", \"\",\n                                        1.0, -1.0, 1.0,\n                                        Direction::AUTOMATABLE,\n                                        nullptr);\n\n    _ch2_gain = register_float_parameter(\"ch2_gain\", \"Channel 2 Gain\", \"\",\n                                         0.0f, -120.0f, 24.0f,\n                                         Direction::AUTOMATABLE,\n                                         new dBToLinPreProcessor(-120.0f, 24.0f));\n\n    _ch2_invert_phase = register_float_parameter(\"ch2_invert_phase\", \"Channel 2 Invert Phase\", \"\",\n                                                 0.0f, 0.0f, 1.0f,\n                                                 Direction::AUTOMATABLE,\n                                                 nullptr);\n\n    assert(_ch1_pan);\n    assert(_ch1_gain);\n    assert(_ch1_invert_phase);\n    assert(_ch2_pan);\n    assert(_ch2_gain);\n    assert(_ch2_invert_phase);\n\n    _ch1_left_gain_smoother.set_direct(1.0f);\n    _ch1_right_gain_smoother.set_direct(0.0f);\n    _ch2_left_gain_smoother.set_direct(0.0f);\n    _ch2_right_gain_smoother.set_direct(1.0f);\n}\n\nProcessorReturnCode StereoMixerPlugin::init(float sample_rate)\n{\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid StereoMixerPlugin::configure(float sample_rate)\n{\n    _ch1_left_gain_smoother.set_lag_time(GAIN_SMOOTHING_TIME, sample_rate / AUDIO_CHUNK_SIZE);\n    _ch1_right_gain_smoother.set_lag_time(GAIN_SMOOTHING_TIME, sample_rate / AUDIO_CHUNK_SIZE);\n    _ch2_left_gain_smoother.set_lag_time(GAIN_SMOOTHING_TIME, sample_rate / AUDIO_CHUNK_SIZE);\n    _ch2_right_gain_smoother.set_lag_time(GAIN_SMOOTHING_TIME, sample_rate / AUDIO_CHUNK_SIZE);\n}\n\nvoid StereoMixerPlugin::process_audio(const ChunkSampleBuffer& input_buffer,\n                                     ChunkSampleBuffer& output_buffer)\n{\n    output_buffer.clear();\n\n    // Calculate parameters\n    float invert_ch1 = (_ch1_invert_phase->processed_value() > 0.5f) ? -1.0f : 1.0f;\n    auto [ch1_left_gain, ch1_right_gain] = calc_l_r_gain(_ch1_gain->processed_value() * invert_ch1,\n                                                         _ch1_pan->processed_value());\n    _ch1_left_gain_smoother.set(ch1_left_gain);\n    _ch1_right_gain_smoother.set(ch1_right_gain);\n\n    float invert_ch2 = (_ch2_invert_phase->processed_value() > 0.5f) ? -1.0f : 1.0f;\n    auto [ch2_left_gain, ch2_right_gain] = calc_l_r_gain(_ch2_gain->processed_value() * invert_ch2,\n                                                         _ch2_pan->processed_value());\n    _ch2_left_gain_smoother.set(ch2_left_gain);\n    _ch2_right_gain_smoother.set(ch2_right_gain);\n\n    if (_bypassed == false)\n    {\n        // Process gain\n        if (input_buffer.channel_count() == 2)\n        {\n            if (_ch1_left_gain_smoother.stationary() &&\n                _ch1_right_gain_smoother.stationary() &&\n                _ch2_left_gain_smoother.stationary() &&\n                _ch2_right_gain_smoother.stationary())\n            {\n                output_buffer.add_with_gain(0, 0, input_buffer, ch1_left_gain);\n                output_buffer.add_with_gain(1, 0, input_buffer, ch1_right_gain);\n                output_buffer.add_with_gain(0, 1, input_buffer, ch2_left_gain);\n                output_buffer.add_with_gain(1, 1, input_buffer, ch2_right_gain);\n            }\n            else // value needs smoothing\n            {\n                output_buffer.add_with_ramp(0, 0, input_buffer, _ch1_left_gain_smoother.value(), _ch1_left_gain_smoother.next_value());\n                output_buffer.add_with_ramp(1, 0, input_buffer, _ch1_right_gain_smoother.value(), _ch1_right_gain_smoother.next_value());\n                output_buffer.add_with_ramp(0, 1, input_buffer, _ch2_left_gain_smoother.value(), _ch2_left_gain_smoother.next_value());\n                output_buffer.add_with_ramp(1, 1, input_buffer, _ch2_right_gain_smoother.value(), _ch2_right_gain_smoother.next_value());\n            }\n        }\n        else // Input is mono\n        {\n            output_buffer.add(input_buffer);\n        }\n    }\n    else\n    {\n        bypass_process(input_buffer, output_buffer);\n    }\n}\n\nstd::string_view StereoMixerPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n} // end namespace sushi::internal::stereo_mixer_plugin\n\n"
  },
  {
    "path": "src/plugins/stereo_mixer_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Stereo mixer\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_STEREO_MIXER_PLUGIN_H\n#define SUSHI_STEREO_MIXER_PLUGIN_H\n\n#include \"library/internal_plugin.h\"\n#include \"dsp_library/value_smoother.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::stereo_mixer_plugin {\n\nclass Accessor;\n\nclass StereoMixerPlugin : public InternalPlugin, public UidHelper<StereoMixerPlugin>\n{\npublic:\n    explicit StereoMixerPlugin(HostControl host_control);\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void process_audio(const ChunkSampleBuffer& in_buffer,ChunkSampleBuffer& out_buffer) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    friend Accessor;\n\n    FloatParameterValue* _ch1_pan;\n    FloatParameterValue* _ch1_gain;\n    FloatParameterValue* _ch1_invert_phase;\n    ValueSmootherFilter<float> _ch1_left_gain_smoother;\n    ValueSmootherFilter<float> _ch1_right_gain_smoother;\n\n    FloatParameterValue* _ch2_pan;\n    FloatParameterValue* _ch2_gain;\n    FloatParameterValue* _ch2_invert_phase;\n    ValueSmootherFilter<float> _ch2_left_gain_smoother;\n    ValueSmootherFilter<float> _ch2_right_gain_smoother;\n};\n\n} // end namespace sushi::internal::stereo_mixer_plugin\n\nELK_POP_WARNING\n\n#endif // SUSHI_STEREO_MIXER_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/transposer_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Midi i/o plugin example\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <algorithm>\n\n#include \"plugins/transposer_plugin.h\"\n#include \"library/midi_decoder.h\"\n#include \"library/midi_encoder.h\"\n\nnamespace sushi::internal::transposer_plugin {\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.transposer\";\nconstexpr auto DEFAULT_LABEL = \"Transposer\";\n\nconstexpr int MAX_NOTE = 127;\nconstexpr int MIN_NOTE = 0;\n\nTransposerPlugin::TransposerPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n    _transpose_parameter = register_float_parameter(\"transpose\",\n                                                    \"Transpose\",\n                                                    \"semitones\",\n                                                    0.0f,\n                                                    -24.0f,\n                                                    24.0f,\n                                                    Direction::AUTOMATABLE,\n                                                    new FloatParameterPreProcessor(-24.0f, 24.0f) );\n    assert(_transpose_parameter);\n    _max_input_channels = 0;\n    _max_output_channels = 0;\n}\n\nProcessorReturnCode TransposerPlugin::init(float /*sample_rate*/)\n{\n    return ProcessorReturnCode::OK;\n}\n\nvoid TransposerPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n        case RtEventType::NOTE_ON:\n        {\n            auto typed_event = event.keyboard_event();\n            output_event(RtEvent::make_note_on_event(typed_event->processor_id(),\n                                                     typed_event->sample_offset(),\n                                                     typed_event->channel(),\n                                                     _transpose_note(typed_event->note()),\n                                                     typed_event->velocity()));\n            break;\n        }\n\n        case RtEventType::NOTE_OFF:\n        {\n            auto typed_event = event.keyboard_event();\n            output_event(RtEvent::make_note_off_event(typed_event->processor_id(),\n                                                      typed_event->sample_offset(),\n                                                      typed_event->channel(),\n                                                      _transpose_note(typed_event->note()),\n                                                      typed_event->velocity()));\n            break;\n        }\n\n        case RtEventType::WRAPPED_MIDI_EVENT:\n        {\n            auto typed_event = event.wrapped_midi_event();\n            output_event(RtEvent::make_wrapped_midi_event(typed_event->processor_id(),\n                                                          typed_event->sample_offset(),\n                                                          _transpose_midi(typed_event->midi_data())));\n            break;\n        }\n\n        default:\n            // Parameter changes are handled by the default implementation\n            InternalPlugin::process_event(event);\n            break;\n    }\n}\n\nint TransposerPlugin::_transpose_note(int note)\n{\n    int steps = static_cast<int>(_transpose_parameter->processed_value());\n    return std::clamp(note + steps, MIN_NOTE, MAX_NOTE);\n}\n\nMidiDataByte TransposerPlugin::_transpose_midi(MidiDataByte midi_msg)\n{\n    auto type = midi::decode_message_type(midi_msg);\n    switch (type)\n    {\n        case midi::MessageType::NOTE_ON:\n        {\n            auto note_on_msg = midi::decode_note_on(midi_msg);\n            return midi::encode_note_on(note_on_msg.channel, _transpose_note(note_on_msg.note), note_on_msg.velocity);\n        }\n        case midi::MessageType::NOTE_OFF:\n        {\n            auto note_off_msg = midi::decode_note_off(midi_msg);\n            return midi::encode_note_off(note_off_msg.channel, _transpose_note(note_off_msg.note), note_off_msg.velocity);\n        }\n        default:\n            return midi_msg;\n    }\n}\n\nvoid TransposerPlugin::process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer)\n{\n    bypass_process(in_buffer, out_buffer);\n}\n\nstd::string_view TransposerPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n} // end namespace sushi::internal::transposer_plugin\n"
  },
  {
    "path": "src/plugins/transposer_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Midi i/o plugin example\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_TRANSPOSER_PLUGIN_H\n#define SUSHI_TRANSPOSER_PLUGIN_H\n\n#include \"library/rt_event_fifo.h\"\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::transposer_plugin {\n\nclass TransposerPlugin : public InternalPlugin, public UidHelper<TransposerPlugin>\n{\npublic:\n    explicit TransposerPlugin(HostControl host_control);\n\n    ~TransposerPlugin() override = default;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer&/*in_buffer*/, ChunkSampleBuffer& /*out_buffer*/) override;\n\n    static std::string_view static_uid();\n\nprivate:\n    int _transpose_note(int note);\n\n    MidiDataByte _transpose_midi(MidiDataByte midi_msg);\n\n    FloatParameterValue* _transpose_parameter;\n};\n\n} // end namespace sushi::internal::transposer_plugin\n\nELK_POP_WARNING\n\n#endif // SUSHI_TRANSPOSER_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/wav_streamer_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Plugin for streaming large wav files from disk\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include \"elklog/static_logger.h\"\n\n#include \"plugins/wav_streamer_plugin.h\"\n\nnamespace sushi::internal::wav_streamer_plugin {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"wav_player\");\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.wav_streamer\";\nconstexpr auto DEFAULT_LABEL = \"Wav Streamer\";\nconstexpr int FILE_PROPERTY_ID = 0;\n\nconstexpr auto  MAX_FADE_TIME = std::chrono::duration<float, std::ratio<1,1>>(100);\nconstexpr auto  MIN_FADE_TIME = std::chrono::duration<float, std::ratio<1,1>>(GAIN_SMOOTHING_TIME);\n\nconstexpr float MAX_FILE_LENGTH = 60 * 60 * 24;\nconstexpr int SEEK_UPDATE_INTERVAL = 200;\n\n// Approximate an exponential audio fade with an x^3 curve. Works pretty good over a 60 dB range.\ninline float exp_approx(float x, float range)\n{\n    float norm = range > 0 ? x / range : 0.0f;\n    return norm * norm * norm * range;\n}\n\n// Catmull-Rom splines, aka Hermite interpolation\ntemplate <typename T>\ninline T catmull_rom_cubic_int(T frac_pos, T d0, T d1, T d2, T d3)\n{\n    T f2 = frac_pos * frac_pos;\n    T a0 = -0.5f * d0 + 1.5f * d1 - 1.5f * d2 + 0.5f * d3;\n    T a1 = d0 - 2.5f * d1 + 2.0f * d2 - 0.5f * d3;\n    T a2 = -0.5f * d0 + 0.5f * d2;\n    T a3 = d1;\n\n    return(a0 * frac_pos * f2 + a1 * f2 + a2 * frac_pos + a3);\n}\n\nint64_t fill_stereo_block(SNDFILE* file, AudioBlock* block, bool looping)\n{\n    sf_count_t sample_count = 0;\n    while (sample_count < BLOCK_SIZE)\n    {\n        auto count = sf_readf_float(file, block->audio_data[INT_MARGIN + sample_count].data(), BLOCK_SIZE - sample_count);\n        sample_count += count;\n        if (sample_count < BLOCK_SIZE)\n        {\n            block->is_last = true;\n            if (looping)\n            {\n                // Start over from the beginning and continue reading.\n                sf_seek(file, 0, SEEK_SET);\n            }\n            else\n            {\n                break;\n            }\n        }\n    }\n\n    return sample_count;\n}\n\nint64_t fill_mono_block(SNDFILE* file, AudioBlock* block, bool looping)\n{\n    sf_count_t sample_count = 0;\n    std::vector<float> tmp_buffer(BLOCK_SIZE, 0.0);\n    while (sample_count < BLOCK_SIZE)\n    {\n        auto count = sf_readf_float(file, tmp_buffer.data() + sample_count, BLOCK_SIZE - sample_count);\n        sample_count += count;\n        if (sample_count < BLOCK_SIZE)\n        {\n            block->is_last = true;\n            if (looping)\n            {\n                sf_seek(file, 0, SEEK_SET);\n            }\n            else\n            {\n                break;\n            }\n        }\n    }\n    // Copy interleaved from the temporary buffer to the block\n    for (int i = 0; i < BLOCK_SIZE; i++)\n    {\n        block->audio_data[i + INT_MARGIN] = {tmp_buffer[i], tmp_buffer[i]};\n    }\n    return sample_count;\n}\n\nvoid fill_remainder(AudioBlock* block, std::array<std::array<float, 2>, INT_MARGIN>& remainder)\n{\n    for (size_t i = 0; i < INT_MARGIN; i++ )\n    {\n        block->audio_data[i] = remainder[i];\n        remainder[i] = block->audio_data[i + BLOCK_SIZE];\n    }\n}\n\nWavStreamerPlugin::WavStreamerPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    _file_info = {0, 0, 0, 0, 0, 0};\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n\n    _remainder.fill({0,0});\n\n    [[maybe_unused]] bool str_pr_ok = register_property(\"file\", \"File\", \"\");\n\n    _gain_parameter  = register_float_parameter(\"volume\", \"Volume\", \"dB\",\n                                                0.0f, -90.0f, 24.0f,\n                                                Direction::AUTOMATABLE,\n                                                new dBToLinPreProcessor(-90.0f, 24.0f));\n\n    _speed_parameter  = register_float_parameter(\"playback_speed\", \"Playback Speed\", \"\",\n                                                  1.0f, 0.5f, 2.0f,\n                                                  Direction::AUTOMATABLE,\n                                                  new FloatParameterPreProcessor(0.5f, 2.0f));\n\n    _fade_parameter   = register_float_parameter(\"fade_time\", \"Fade Time\", \"s\",\n                                                  0.0f, 0.0f, MAX_FADE_TIME.count(),\n                                                  Direction::AUTOMATABLE,\n                                                  new FloatParameterPreProcessor(0.0f, MAX_FADE_TIME.count()));\n\n    _seek_parameter   = register_float_parameter(\"seek\", \"Seek\", \"\",\n                                                 0.0f, 0.0f, 1.0f,\n                                                 Direction::AUTOMATABLE,\n                                                 new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    _pos_parameter   = register_float_parameter(\"position\", \"Position\", \"\",\n                                                0.0f, 0.0f, 1.0f,\n                                                Direction::OUTPUT,\n                                                new FloatParameterPreProcessor(0.0f, 1.0f));\n\n    _length_parameter = register_float_parameter(\"length\", \"Length\", \"s\",\n                                                 0.0f, 0.0f, MAX_FILE_LENGTH,\n                                                 Direction::OUTPUT,\n                                                 new FloatParameterPreProcessor(0.0f, MAX_FILE_LENGTH));\n\n    _start_stop_parameter = register_bool_parameter(\"playing\", \"Playing\", \"\", false, Direction::AUTOMATABLE);\n    _loop_parameter = register_bool_parameter(\"loop\", \"Loop\", \"\", false, Direction::AUTOMATABLE);\n    _exp_fade_parameter = register_bool_parameter(\"exp_fade\", \"Exponential fade\", \"\", false, Direction::AUTOMATABLE);\n\n    assert(_gain_parameter && _speed_parameter && _fade_parameter && _pos_parameter &&\n           _start_stop_parameter &&_loop_parameter && _exp_fade_parameter && str_pr_ok);\n    _max_input_channels = 0;\n}\n\nWavStreamerPlugin::~WavStreamerPlugin()\n{\n    if (_file)\n    {\n        std::scoped_lock lock(_file_mutex);\n        _close_audio_file();\n    }\n    AudioBlock* block;\n    while (_block_queue.pop(block))\n    {\n        delete block;\n    }\n}\n\nProcessorReturnCode WavStreamerPlugin::init(float sample_rate)\n{\n    configure(sample_rate);\n    return ProcessorReturnCode::OK;\n}\n\nvoid WavStreamerPlugin::configure(float sample_rate)\n{\n    _sample_rate = sample_rate;\n    _gain_smoother.set_lag_time(GAIN_SMOOTHING_TIME, sample_rate / AUDIO_CHUNK_SIZE);\n    _exp_gain_smoother.set_lag_time(GAIN_SMOOTHING_TIME, sample_rate / AUDIO_CHUNK_SIZE);\n}\n\nvoid WavStreamerPlugin::set_enabled(bool enabled)\n{\n    Processor::set_enabled(enabled);\n}\n\nvoid WavStreamerPlugin::set_bypassed(bool bypassed)\n{\n    _host_control.post_event(std::make_unique<SetProcessorBypassEvent>(this->id(), bypassed, IMMEDIATE_PROCESS));\n}\n\nvoid WavStreamerPlugin::process_event(const RtEvent& event)\n{\n    switch (event.type())\n    {\n        case RtEventType::SET_BYPASS:\n        {\n            bool bypassed = static_cast<bool>(event.processor_command_event()->value());\n            Processor::set_bypassed(bypassed);\n            _bypass_manager.set_bypass(bypassed, _sample_rate);\n            break;\n        }\n        case RtEventType::BOOL_PARAMETER_CHANGE:\n        case RtEventType::INT_PARAMETER_CHANGE:\n        case RtEventType::FLOAT_PARAMETER_CHANGE:\n        {\n            InternalPlugin::process_event(event);\n            auto typed_event = event.parameter_change_event();\n            if (typed_event->param_id() == _start_stop_parameter->descriptor()->id())\n            {\n                _start_stop_playing(_start_stop_parameter->processed_value());\n            }\n            else if (typed_event->param_id() == _seek_parameter->descriptor()->id())\n            {\n                _seek_in_process = true;\n                request_non_rt_task(set_seek_callback);\n            }\n            break;\n        }\n\n        default:\n            InternalPlugin::process_event(event);\n            break;\n    }\n}\n\nvoid WavStreamerPlugin::process_audio([[maybe_unused]] const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer)\n{\n    // If there is no current block, or the current block is outdated.\n    if (!_current_block || (_current_block && _current_block->file_idx < _file_idx))\n    {\n        _load_new_block();\n        _update_file_length_display();\n    }\n\n    if (_current_block && _bypass_manager.should_process() && _mode != sushi::internal::wav_streamer_plugin::StreamingMode::STOPPED)\n    {\n        if (_mode == StreamingMode::PLAYING || _mode == StreamingMode::STARTING)\n        {\n            float gain_value = _gain_parameter->processed_value();\n            if (_seek_in_process)\n            {\n                gain_value = 0.0f;\n            }\n\n            _gain_smoother.set(gain_value);\n            _exp_gain_smoother.set(gain_value);\n        }\n\n        _fill_audio_data(out_buffer, _file_samplerate / _sample_rate * _speed_parameter->processed_value());\n\n        _handle_fades(out_buffer);\n    }\n    else\n    {\n        out_buffer.clear();\n    }\n\n    if (++_seek_update_count > SEEK_UPDATE_INTERVAL)\n    {\n        _update_position_display(_loop_parameter->processed_value());\n        _seek_update_count = 0;\n    }\n\n    _mode = _update_mode(_mode);\n}\n\nProcessorReturnCode WavStreamerPlugin::set_property_value(ObjectId property_id, const std::string& value)\n{\n    auto status = InternalPlugin::set_property_value(property_id, value);\n    if (status == ProcessorReturnCode::OK && property_id == FILE_PROPERTY_ID)\n    {\n        _open_audio_file(value);\n        _read_audio_data();\n    }\n    return status;\n}\n\nint WavStreamerPlugin::set_seek_callback(void* data, [[maybe_unused]] EventId id)\n{\n    assert(data);\n    auto instance = reinterpret_cast<WavStreamerPlugin*>(data);\n    instance->_set_seek();\n    instance->_read_audio_data();\n    return 0;\n}\n\nint WavStreamerPlugin::read_data_callback(void* data, [[maybe_unused]] EventId id)\n{\n    assert(data);\n    auto instance = reinterpret_cast<WavStreamerPlugin*>(data);\n    instance->_read_audio_data();\n    return 0;\n}\n\nstd::string_view WavStreamerPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\nbool WavStreamerPlugin::_open_audio_file(const std::string& path)\n{\n    std::scoped_lock lock(_file_mutex);\n\n    if (_file)\n    {\n        _close_audio_file();\n    }\n\n    _file = sf_open(path.c_str(), SFM_READ, &_file_info);\n    _file_idx += 1;\n\n    if (_file == nullptr || _file_info.channels > 2)\n    {\n        _file_length = 0.0f;\n        std::string str_error;\n        if (_file_info.channels > 2)\n        {\n            str_error = \"Multichannel files not supported\";\n        }\n        else\n        {\n            str_error = sf_strerror(nullptr);\n        }\n        InternalPlugin::set_property_value(FILE_PROPERTY_ID, \"Error: \" + str_error);\n        ELKLOG_LOG_ERROR(\"Failed to load audio file: {}, error: {}\", path, str_error);\n        return false;\n    }\n\n    _file_samplerate = static_cast<float>(_file_info.samplerate);\n    _file_length = static_cast<float>(_file_info.frames);\n    // The file length parameter will be updated from the audio thread\n\n    ELKLOG_LOG_INFO(\"Opened file: {}, {} channels, {} frames, {} Hz\", path, _file_info.channels, _file_info.frames, _file_info.samplerate);\n    return true;\n}\n\nvoid WavStreamerPlugin::_close_audio_file()\n{\n    sf_close(_file);\n    _file_length = 0;\n    _file_pos = 0;\n}\n\nint WavStreamerPlugin::_read_audio_data()\n{\n    std::scoped_lock lock(_file_mutex);\n\n    bool looping = _loop_parameter->processed_value();\n    if (_file)\n    {\n        int blockcount = MAX_BLOCKS_PER_LOAD;\n        while (!_block_queue.wasFull() && blockcount-- > 0)\n        {\n            auto block = new AudioBlock;\n            block->file_pos = sf_seek(_file, 0, SEEK_CUR);\n            block->file_idx = _file_idx;\n\n            sf_count_t samplecount;\n            if (_file_info.channels == 1)\n            {\n                samplecount = fill_mono_block(_file, block, looping);\n            }\n            else\n            {\n                samplecount = fill_stereo_block(_file, block, looping);\n            }\n            // Blocks overlap to make interpolation easier\n            fill_remainder(block, _remainder);\n            _block_queue.push(block);\n\n            if (samplecount < BLOCK_SIZE)\n            {\n                break;\n            }\n        }\n    }\n    return 0;\n}\n\nvoid WavStreamerPlugin::_fill_audio_data(ChunkSampleBuffer& buffer, float speed)\n{\n    bool stereo = buffer.channel_count() > 1;\n\n    for (int s = 0; s < AUDIO_CHUNK_SIZE; s++)\n    {\n        const auto& data = _current_block->audio_data;\n\n        auto first = static_cast<int>(_current_block_pos);\n        float frac_pos = _current_block_pos - std::floor(_current_block_pos);\n        assert(first >= 0);\n        assert(first < BLOCK_SIZE);\n\n        float left = catmull_rom_cubic_int(frac_pos, data[first][LEFT_CHANNEL_INDEX], data[first + 1][LEFT_CHANNEL_INDEX],\n                                           data[first + 2][LEFT_CHANNEL_INDEX], data[first + 3][LEFT_CHANNEL_INDEX]);\n\n        float right = catmull_rom_cubic_int(frac_pos, data[first][RIGHT_CHANNEL_INDEX], data[first + 1][RIGHT_CHANNEL_INDEX],\n                                            data[first + 2][RIGHT_CHANNEL_INDEX], data[first + 3][RIGHT_CHANNEL_INDEX]);\n\n        if (stereo)\n        {\n            buffer.channel(LEFT_CHANNEL_INDEX)[s] = left;\n            buffer.channel(RIGHT_CHANNEL_INDEX)[s] = right;\n        }\n        else\n        {\n            buffer.channel(LEFT_CHANNEL_INDEX)[s] = 0.5f * (left + right);\n        }\n\n        _current_block_pos += speed;\n        if (_current_block_pos >= BLOCK_SIZE)\n        {\n            // Don't reset to 0, as we want to preserve the fractional position.\n            _current_block_pos -= BLOCK_SIZE;\n            if (!_load_new_block())\n            {\n                break;\n            }\n        }\n    }\n    _file_pos += speed * AUDIO_CHUNK_SIZE;\n}\n\nStreamingMode WavStreamerPlugin::_update_mode(StreamingMode current)\n{\n    switch (current)\n    {\n        case StreamingMode::STARTING:\n            if (_gain_smoother.stationary())\n            {\n                _gain_smoother.set_lag_time(GAIN_SMOOTHING_TIME, _sample_rate / AUDIO_CHUNK_SIZE);\n                _exp_gain_smoother.set_lag_time(GAIN_SMOOTHING_TIME, _sample_rate / AUDIO_CHUNK_SIZE);\n                return StreamingMode::PLAYING;\n            }\n            break;\n\n        case StreamingMode::STOPPING:\n            if (_gain_smoother.stationary())\n            {\n                return StreamingMode::STOPPED;\n            }\n            break;\n\n        default: {}\n\n    }\n    return current;\n}\n\nbool WavStreamerPlugin::_load_new_block()\n{\n    auto prev_block = _current_block;\n    AudioBlock* new_block = nullptr;\n\n    while (_block_queue.pop(new_block))\n    {\n        if (new_block->file_idx == _file_idx)\n        {\n            _file_pos = static_cast<float>(new_block->file_pos);\n            _update_file_length_display();\n            break;\n        }\n        else // Block is stale due to seek or new file\n        {\n            _current_block_pos = 0.0f;\n            _seek_in_process = false;\n            if (prev_block)\n            {\n                async_delete(prev_block);\n            }\n            prev_block = new_block;\n            new_block = nullptr;\n        }\n    }\n\n    _current_block = new_block;\n\n    if (prev_block)\n    {\n        if ((prev_block->is_last && !_loop_parameter->processed_value()) || _file == nullptr)\n        {\n            _handle_end_of_file();\n        }\n        async_delete(prev_block);\n    }\n\n    if (_block_queue.wasEmpty())\n    {\n        // Schedule a task to load more blocks.\n        request_non_rt_task(read_data_callback);\n    }\n\n    return _current_block;\n}\n\nvoid WavStreamerPlugin::_start_stop_playing(bool start)\n{\n    auto lag = std::max(MIN_FADE_TIME, MAX_FADE_TIME * _fade_parameter->normalized_value());\n\n    if (start && (_mode != StreamingMode::PLAYING && _mode != StreamingMode::STARTING))\n    {\n        _mode = StreamingMode::STARTING;\n        _gain_smoother.set_lag_time(lag, _sample_rate / AUDIO_CHUNK_SIZE);\n        _gain_smoother.set(_gain_parameter->processed_value());\n        _exp_gain_smoother.set_lag_time(lag, _sample_rate / AUDIO_CHUNK_SIZE);\n        _exp_gain_smoother.set(_gain_parameter->processed_value());\n    }\n\n    if (!start && (_mode != StreamingMode::STOPPED && _mode != StreamingMode::STOPPING))\n    {\n        _mode = StreamingMode::STOPPING;\n        _gain_smoother.set_lag_time(lag, _sample_rate / AUDIO_CHUNK_SIZE);\n        _gain_smoother.set(0.0f);\n        _exp_gain_smoother.set_lag_time(lag, _sample_rate / AUDIO_CHUNK_SIZE);\n        _exp_gain_smoother.set(0.0f);\n    }\n}\n\nvoid WavStreamerPlugin::_update_position_display(bool looping)\n{\n    float position = 0;\n    if (_file_length > 0.0f)\n    {\n        // If looping is on, the last block will contain both the start and the end of the files, so let position wraparound\n        if (looping)\n        {\n            position = std::fmod(_file_pos / _file_length, 1.0f);\n        }\n        else // The last block will contain a bit of silence at the end, don't let position go past 1.0\n        {\n            position = std::clamp(_file_pos / _file_length, 0.0f, 1.0f);\n        }\n    }\n\n    if (position != _pos_parameter->normalized_value())\n    {\n        set_parameter_and_notify(_pos_parameter, position);\n    }\n}\n\nvoid WavStreamerPlugin::_update_file_length_display()\n{\n    float length = 0.0;\n    if (_file_length > 0.0 && _file_samplerate > 0.0)\n    {\n        length = _file_length / _file_samplerate / MAX_FILE_LENGTH;\n    }\n    if (length != _length_parameter->normalized_value())\n    {\n        set_parameter_and_notify(_length_parameter, length);\n    }\n}\n\nvoid WavStreamerPlugin::_set_seek()\n{\n    std::scoped_lock lock(_file_mutex);\n    if (_file)\n    {\n        float pos = _seek_parameter->normalized_value();\n        ELKLOG_LOG_DEBUG(\"Setting seek to {}\", pos);\n        sf_seek(_file, static_cast<sf_count_t>(std::floor(pos * _file_length)), SEEK_SET);\n        _file_idx +=1;\n    }\n}\n\nvoid WavStreamerPlugin::_handle_end_of_file()\n{\n    _mode = StreamingMode::STOPPED;\n    _gain_smoother.set_direct(0.0f);\n    _exp_gain_smoother.set_direct(0.0f);\n    _file_pos = 0;\n\n    set_parameter_and_notify(_start_stop_parameter, false);\n    request_non_rt_task(set_seek_callback);\n    _update_position_display(false);\n}\n\nvoid WavStreamerPlugin::_handle_fades(ChunkSampleBuffer& buffer)\n{\n    if (_gain_smoother.stationary()) // Both smoothers run with the same lag, so both should be stationary\n    {\n        buffer.apply_gain(_gain_smoother.value());\n    }\n    else // Ramp because start/stop, gain parameter changed or quick down/up fade due to seeking\n    {\n        float start;\n        float end;\n        if (_exp_fade_parameter->processed_value()) // Exponential fade is enabled\n        {\n            start = _exp_gain_smoother.value();\n            end = _exp_gain_smoother.next_value();\n            _gain_smoother.next_value();\n        }\n        else\n        {\n            start = _gain_smoother.value();\n            end = _gain_smoother.next_value();\n            _exp_gain_smoother.next_value();\n        }\n        buffer.ramp(start, end);\n    }\n\n    if (_bypass_manager.should_ramp()) // Ramp because bypass was triggered\n    {\n        _bypass_manager.ramp_output(buffer);\n    }\n}\n\n} // namespace sushi::internal::wav_player_plugin\n"
  },
  {
    "path": "src/plugins/wav_streamer_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Plugin for streaming large wav files from disk\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_WAV_STREAMER_PLUGIN_H\n#define SUSHI_WAV_STREAMER_PLUGIN_H\n\n#include <array>\n#include <sndfile.h>\n\n#include \"fifo/circularfifo_memory_relaxed_aquire_release.h\"\n\n#include \"dsp_library/value_smoother.h\"\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::wav_streamer_plugin {\n\nenum class StreamingMode\n{\n    PLAYING,\n    STARTING,\n    STOPPING,\n    STOPPED\n};\n\n// Roughly 2 seconds of stereo audio per block @ 48kHz.\nconstexpr sf_count_t BLOCK_SIZE = 100'000;\nconstexpr size_t QUEUE_SIZE = 32;\nconstexpr int MAX_BLOCKS_PER_LOAD = 4;\n\n// Extra margin for interpolation\nconstexpr size_t PRE_SAMPLES = 1;\nconstexpr size_t POST_SAMPLES = 2;\nconstexpr size_t INT_MARGIN = PRE_SAMPLES + POST_SAMPLES;\n\n/**\n * @brief A block of stereo audio data with some basic control data\n */\nstruct AudioBlock : public RtDeletable\n{\n    AudioBlock() : file_pos(0), file_idx(0), is_last(false)\n    {\n        audio_data.fill({0.0f, 0.0f});\n    }\n\n    int64_t file_pos;\n    int file_idx;\n    bool is_last;\n    std::array<std::array<float, 2>, BLOCK_SIZE + INT_MARGIN> audio_data;\n};\n\n/**\n * @brief Fill an AudioBlock with data from a stereo file\n * @param file  An open SDNFILE object\n * @param block An allocated AudioBlock to be populated\n * @param looping If true, reading starts over from the beginning of the file if the end is reached\n * @return The number of frames read\n */\nint64_t fill_stereo_block(SNDFILE* file, AudioBlock* block, bool looping);\n\n/**\n * @brief Fill an AudioBlock with data from a mono file\n * @param file  An open SDNFILE object\n * @param block An allocated AudioBlock to be populated\n * @param looping If true, reading starts over from the beginning of the file if the end is reached\n * @return The number of frames read\n */\nint64_t fill_mono_block(SNDFILE* file, AudioBlock* block, bool looping);\n\nvoid fill_remainder(AudioBlock* block, std::array<std::array<float, 2>, INT_MARGIN>& remainder);\n\nclass WavStreamerPlugin : public InternalPlugin, public UidHelper<WavStreamerPlugin>\n{\npublic:\n    explicit WavStreamerPlugin(HostControl host_control);\n\n    ~WavStreamerPlugin() override;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_enabled(bool enabled) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_event(const RtEvent& event) override;\n\n    void process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer) override;\n\n    ProcessorReturnCode set_property_value(ObjectId property_id, const std::string& value) override;\n\n    static int read_data_callback(void* data, EventId id);\n\n    static int set_seek_callback(void* data, EventId id);\n\n    static std::string_view static_uid();\n\nprivate:\n    bool _open_audio_file(const std::string& path);\n\n    void _close_audio_file();\n\n    int _read_audio_data();\n\n    void _fill_audio_data(ChunkSampleBuffer& buffer, float speed);\n\n    StreamingMode _update_mode(StreamingMode current);\n\n    bool _load_new_block();\n\n    void _start_stop_playing(bool start);\n\n    void _update_position_display(bool looping);\n\n    void _update_file_length_display();\n\n    void _set_seek();\n\n    void _handle_fades(ChunkSampleBuffer& buffer);\n\n    void _handle_end_of_file();\n\n    ValueSmootherRamp<float>    _gain_smoother;\n    ValueSmootherExpRamp<float> _exp_gain_smoother;\n\n    FloatParameterValue* _gain_parameter;\n    FloatParameterValue* _speed_parameter;\n    FloatParameterValue* _fade_parameter;\n    FloatParameterValue* _pos_parameter;\n    FloatParameterValue* _seek_parameter;\n    FloatParameterValue* _length_parameter;\n    BoolParameterValue*  _start_stop_parameter;\n    BoolParameterValue*  _loop_parameter;\n    BoolParameterValue*  _exp_fade_parameter;\n\n    float _sample_rate {0};\n    float _file_samplerate {0};\n    float _file_length {1};\n    int   _file_idx {0};\n\n    std::array<std::array<float, 2>, INT_MARGIN> _remainder {};\n\n    std::mutex  _file_mutex;\n    SNDFILE*    _file {nullptr};\n    SF_INFO     _file_info {};\n\n    BypassManager _bypass_manager;\n\n    StreamingMode _mode {sushi::internal::wav_streamer_plugin::StreamingMode::STOPPED};\n\n    AudioBlock* _current_block {nullptr};\n    float _current_block_pos {0};\n    float _file_pos {0};\n\n    int _seek_update_count {0};\n    bool _seek_in_process {false};\n\n    memory_relaxed_aquire_release::CircularFifo<AudioBlock*, QUEUE_SIZE> _block_queue;\n};\n\n} // namespace sushi::internal::wav_player_plugin\n\nELK_POP_WARNING\n\n#endif //SUSHI_WAV_STREAMER_PLUGIN_H\n"
  },
  {
    "path": "src/plugins/wav_writer_plugin.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Simple plugin for writing wav files.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <cstring>\n\n#include \"elklog/static_logger.h\"\n\n#include \"plugins/wav_writer_plugin.h\"\n\nnamespace sushi::internal::wav_writer_plugin {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"wav_writer\");\n\nconstexpr auto PLUGIN_UID = \"sushi.testing.wav_writer\";\nconstexpr auto DEFAULT_LABEL = \"Wav writer\";\nconstexpr auto DEFAULT_PATH = \"./\";\nconstexpr int DEST_FILE_PROPERTY_ID = 0;\n\nWavWriterPlugin::WavWriterPlugin(HostControl host_control) : InternalPlugin(host_control)\n{\n    Processor::set_name(PLUGIN_UID);\n    Processor::set_label(DEFAULT_LABEL);\n    _max_input_channels = N_AUDIO_CHANNELS;\n    _max_output_channels = N_AUDIO_CHANNELS;\n\n    [[maybe_unused]] bool str_pr_ok = register_property(\"destination_file\", \"Destination file\", \"\");\n    _recording_parameter = register_bool_parameter(\"recording\", \"Recording\", \"bool\", false, Direction::AUTOMATABLE);\n    _write_speed_parameter = register_float_parameter(\"write_speed\", \"Write Speed\", \"writes/s\",\n                                                      DEFAULT_WRITE_INTERVAL,\n                                                      MIN_WRITE_INTERVAL,\n                                                      MAX_WRITE_INTERVAL,\n                                                      Direction::AUTOMATABLE);\n\n    assert(_recording_parameter && _write_speed_parameter && str_pr_ok);\n}\n\nWavWriterPlugin::~WavWriterPlugin()\n{\n    _stop_recording();\n}\n\nProcessorReturnCode WavWriterPlugin::init(float sample_rate)\n{\n    memset(&_soundfile_info, 0, sizeof(_soundfile_info));\n    _soundfile_info.samplerate = static_cast<int>(sample_rate);\n    _soundfile_info.channels = N_AUDIO_CHANNELS;\n    _soundfile_info.format = (SF_FORMAT_WAV | SF_FORMAT_PCM_24);\n    _write_speed = _write_speed_parameter->domain_value();\n\n    return ProcessorReturnCode::OK;\n}\n\nvoid WavWriterPlugin::configure(float sample_rate)\n{\n    _soundfile_info.samplerate = static_cast<int>(sample_rate);\n}\n\nvoid WavWriterPlugin::set_bypassed(bool bypassed)\n{\n    Processor::set_bypassed(bypassed);\n}\n\nvoid WavWriterPlugin::process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer)\n{\n    bypass_process(in_buffer, out_buffer);\n    // Put samples in the ringbuffer already in interleaved format\n    if (_recording_parameter->processed_value())\n    {\n        std::array<float, AUDIO_CHUNK_SIZE * N_AUDIO_CHANNELS> temp_buffer;\n\n        // If input is mono put the same audio in both left and right channels.\n        if (in_buffer.channel_count() == 1)\n        {\n            for (int i = 0; i < AUDIO_CHUNK_SIZE; ++i)\n            {\n                float sample = in_buffer.channel(0)[i];\n                temp_buffer[i*2] = sample;\n                temp_buffer[i*2+1] = sample;\n            }\n        }\n        else\n        {\n            in_buffer.to_interleaved(&temp_buffer[0]);\n        }\n        _ring_buffer.push(temp_buffer);\n    }\n\n    // Post RtEvent to write at an interval specified by POST_WRITE_FREQUENCY\n    if (_post_write_timer > POST_WRITE_FREQUENCY)\n    {\n        _post_write_event();\n        _post_write_timer = 0;\n    }\n    _post_write_timer++;\n}\n\nWavWriterStatus WavWriterPlugin::_start_recording()\n{\n    std::string destination_file_path = property_value(DEST_FILE_PROPERTY_ID).second;\n    if (destination_file_path.empty())\n    {\n        // If no file name was passed. Set it to the default;\n        destination_file_path = std::string(DEFAULT_PATH + this->name() + \"_output\");\n    }\n\n    _actual_file_path = _available_path(destination_file_path);\n    _output_file = sf_open(_actual_file_path.c_str(), SFM_WRITE, &_soundfile_info);\n    if (_output_file == nullptr)\n    {\n        ELKLOG_LOG_ERROR(\"libsndfile error: {}\", sf_strerror(_output_file));\n        return WavWriterStatus::FAILURE;\n    }\n    ELKLOG_LOG_INFO(\"Started recording to file: {}\", _actual_file_path);\n    return WavWriterStatus::SUCCESS;\n}\n\nWavWriterStatus WavWriterPlugin::_stop_recording()\n{\n    _write_to_file(); // write any leftover samples\n    int status = sf_close(_output_file);\n    if (status != 0)\n    {\n        ELKLOG_LOG_ERROR(\"libsndfile error: {}\", sf_error_number(status));\n        return WavWriterStatus::FAILURE;\n    }\n    ELKLOG_LOG_INFO(\"Finished recording to file: {}\", _actual_file_path);\n    _output_file = nullptr;\n    return WavWriterStatus::SUCCESS;\n}\n\nvoid WavWriterPlugin::_post_write_event()\n{\n    auto e = RtEvent::make_async_work_event(&WavWriterPlugin::non_rt_callback, this->id(), this);\n    output_event(e);\n}\n\nint WavWriterPlugin::_write_to_file()\n{\n    std::array<float, AUDIO_CHUNK_SIZE * N_AUDIO_CHANNELS> cur_buffer;\n    while (_ring_buffer.pop(cur_buffer))\n    {\n        for (auto cur_sample : cur_buffer)\n        {\n            _file_buffer.push_back(cur_sample);\n            _samples_received++;\n        }\n    }\n\n    _samples_written = 0;\n\n    unsigned int write_limit = static_cast<int>(_write_speed * _soundfile_info.samplerate);\n    if (_samples_received > write_limit || _recording_parameter->domain_value() == false)\n    {\n        while (_samples_written < _samples_received)\n        {\n            sf_count_t samples_to_write = static_cast<sf_count_t>(_samples_received - _samples_written);\n            if (sf_error(_output_file) == 0)\n            {\n                _samples_written += sf_write_float(_output_file,\n                                                  &_file_buffer[_samples_written],\n                                                   samples_to_write);\n            }\n            else\n            {\n                ELKLOG_LOG_ERROR(\"libsndfile: {}\", sf_strerror(_output_file));\n                return 0;\n            }\n\n        }\n        sf_write_sync(_output_file);\n        _file_buffer.clear();\n        _samples_received = 0;\n    }\n\n    return static_cast<int>(_samples_written);\n}\n\nint WavWriterPlugin::_non_rt_callback(EventId /* id */)\n{\n    WavWriterStatus status = WavWriterStatus::SUCCESS;\n    if (_recording_parameter->domain_value() && _total_samples_written < SAMPLE_WRITE_LIMIT)\n    {\n        if (_output_file == nullptr)\n        {\n            // only change write speed before recording starts\n            _write_speed = _write_speed_parameter->domain_value();\n            status = _start_recording();\n            if (status == WavWriterStatus::FAILURE)\n            {\n                return status;\n            }\n        }\n        int samples_written = _write_to_file();\n        if (samples_written > 0)\n        {\n            ELKLOG_LOG_DEBUG(\"Sucessfully wrote {} samples\", samples_written);\n        }\n        _total_samples_written += samples_written;\n    }\n    else\n    {\n        if (_output_file)\n        {\n            status = _stop_recording();\n            _total_samples_written = 0;\n        }\n    }\n    return status;\n}\n\nstd::string WavWriterPlugin::_available_path(const std::string& requested_path)\n{\n    std::string suffix = \".wav\";\n    std::string new_path = requested_path + suffix;\n    SF_INFO temp_info;\n    SNDFILE* temp_file = sf_open(new_path.c_str(), SFM_READ, &temp_info);\n    int suffix_counter = 1;\n    while (sf_error(temp_file) == 0)\n    {\n        int status = sf_close(temp_file);\n        if (status != 0)\n        {\n            ELKLOG_LOG_ERROR(\"libsndfile error: {} {}\",status, sf_error_number(status));\n        }\n        ELKLOG_LOG_DEBUG(\"File {} already exists\", new_path);\n        new_path = requested_path + \"_\" + std::to_string(suffix_counter) + suffix;\n        temp_file = sf_open(new_path.c_str(), SFM_READ, &temp_info);\n        suffix_counter++;\n    }\n    int status = sf_close(temp_file);\n    if (status != 0)\n    {\n        ELKLOG_LOG_ERROR(\"libsndfile error: {}\", sf_error_number(status));\n    }\n    return new_path;\n}\n\nstd::string_view WavWriterPlugin::static_uid()\n{\n    return PLUGIN_UID;\n}\n\n} // end namespace sushi::internal::wav_writer_plugin\n"
  },
  {
    "path": "src/plugins/wav_writer_plugin.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Simple plugin for writing wav files.\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_WAVE_WRITER_PLUGIN_H\n#define SUSHI_WAVE_WRITER_PLUGIN_H\n\n#include <sndfile.h>\n\n#include \"fifo/circularfifo_memory_relaxed_aquire_release.h\"\n\n#include \"library/internal_plugin.h\"\n\nELK_PUSH_WARNING\nELK_DISABLE_DOMINANCE_INHERITANCE\n\nnamespace sushi::internal::wav_writer_plugin {\n\nconstexpr int N_AUDIO_CHANNELS = 2;\nconstexpr int RINGBUFFER_SIZE = 65536 / AUDIO_CHUNK_SIZE;\nconstexpr int POST_WRITE_FREQUENCY = (RINGBUFFER_SIZE / 4);\nconstexpr int SAMPLE_WRITE_LIMIT = 48000 * N_AUDIO_CHANNELS * 3600; // Limit file size to 1 hour of stereo audio\nconstexpr float DEFAULT_WRITE_INTERVAL = 1.0f;\nconstexpr float MAX_WRITE_INTERVAL = 4.0f;\nconstexpr float MIN_WRITE_INTERVAL = 0.5f;\n\nenum WavWriterStatus : int\n{\n    SUCCESS = 0,\n    FAILURE\n};\n\nclass Accessor;\n\nclass WavWriterPlugin : public InternalPlugin, public UidHelper<WavWriterPlugin>\n{\npublic:\n    explicit WavWriterPlugin(HostControl host_control);\n\n    ~WavWriterPlugin() override;\n\n    ProcessorReturnCode init(float sample_rate) override;\n\n    void configure(float sample_rate) override;\n\n    void set_bypassed(bool bypassed) override;\n\n    void process_audio(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer) override;\n\n    static int non_rt_callback(void* data, EventId id)\n    {\n        return reinterpret_cast<WavWriterPlugin*>(data)->_non_rt_callback(id);\n    }\n\n    static std::string_view static_uid();\n\nprivate:\n    friend Accessor;\n\n    WavWriterStatus _start_recording();\n    WavWriterStatus _stop_recording();\n    void _post_write_event();\n    int _write_to_file();\n    int _non_rt_callback(EventId id);\n    std::string _available_path(const std::string& requested_path);\n\n    memory_relaxed_aquire_release::CircularFifo<std::array<float, AUDIO_CHUNK_SIZE * N_AUDIO_CHANNELS>, RINGBUFFER_SIZE> _ring_buffer;\n\n    std::vector<float> _file_buffer;\n    SNDFILE* _output_file {nullptr};\n    SF_INFO _soundfile_info;\n\n    BoolParameterValue* _recording_parameter;\n    FloatParameterValue* _write_speed_parameter;\n    std::string _actual_file_path;\n\n    float _write_speed {0.0f};\n\n    int _post_write_timer {0};\n    unsigned int _samples_received {0};\n    sf_count_t _samples_written {0};\n    sf_count_t _total_samples_written {0};\n};\n\n} // end namespace sushi::internal::wav_writer_plugin\n\nELK_POP_WARNING\n\n#endif // SUSHI_WAVE_WRITER_PLUGIN_H\n"
  },
  {
    "path": "src/utils.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Utility functions around rapidjson library\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#include <optional>\n#include <iostream>\n#include <fstream>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_TYPE_LIMITS\n#include \"rapidjson/ostreamwrapper.h\"\n#include \"rapidjson/prettywriter.h\"\n#include \"rapidjson/document.h\"\nELK_POP_WARNING\n\n#include \"elklog/static_logger.h\"\n#include \"sushi/sushi.h\"\n\n#ifdef SUSHI_BUILD_WITH_SENTRY\n#include \"sushi/elk_sentry_log_sink.h\"\n#endif\n\nnamespace sushi {\n\nELKLOG_GET_LOGGER_WITH_MODULE_NAME(\"utils\");\n\nstd::ostream& operator<<(std::ostream& out, const rapidjson::Document& document)\n{\n    rapidjson::OStreamWrapper osw(out);\n    rapidjson::PrettyWriter<rapidjson::OStreamWrapper> writer(osw);\n    document.Accept(writer);\n    return out;\n}\n\nstd::optional<std::string> read_file(const std::string& path)\n{\n    std::ifstream config_file(path);\n    if (!config_file.good())\n    {\n        ELKLOG_LOG_ERROR(\"Invalid path passed to file {}\", path);\n        return std::nullopt;\n    }\n\n    // Iterate through every char in file and store in the string\n    std::string config_file_contents((std::istreambuf_iterator<char>(config_file)), std::istreambuf_iterator<char>());\n\n    return config_file_contents;\n}\n\nvoid init_logger([[maybe_unused]] const SushiOptions& options)\n{\n    auto ret_code = elklog::StaticLogger::init_logger(options.log_file,\n                                                      \"Logger\",\n                                                      options.log_level,\n                                                      options.enable_flush_interval? options.log_flush_interval : std::chrono::seconds(0));\n\n    if (ret_code != elklog::Status::OK)\n    {\n        std::cerr << \"Log failure \" << ret_code << \", using default.\" << std::endl;\n    }\n\n    if (options.enable_flush_interval)\n    {\n        ELKLOG_LOG_INFO(\"Logger flush interval enabled, at {} seconds.\",\n                        options.log_flush_interval.count());\n    }\n    else\n    {\n        ELKLOG_LOG_INFO(\"Logger flush interval disabled.\");\n    }\n\n#ifdef SUSHI_BUILD_WITH_SENTRY\n    auto sentry_sink = std::make_shared<elk::sentry_sink_mt>();\n    ELKLOG_ADD_SINK(sentry_sink);\n#endif\n\n}\n\n}"
  },
  {
    "path": "test/CMakeLists.txt",
    "content": "#####################################\n#  Unit Tests Targets               #\n#####################################\n\n# build gtest framework\nadd_subdirectory(gtest)\nenable_testing()\n\n#####################\n#  Unit Test Files  #\n#####################\n\nSET(TEST_FILES\n    unittests/sample_test.cpp\n    unittests/plugins/arpeggiator_plugin_test.cpp\n    unittests/plugins/control_to_cv_plugin_test.cpp\n    unittests/plugins/cv_to_control_plugin_test.cpp\n    unittests/plugins/plugins_test.cpp\n    unittests/plugins/external_plugins_test.cpp\n    unittests/plugins/brickworks_simple_synth_test.cpp\n    unittests/plugins/sample_player_plugin_test.cpp\n    unittests/plugins/send_return_test.cpp\n    unittests/plugins/step_sequencer_test.cpp\n    unittests/plugins/wav_streamer_plugin_test.cpp\n    unittests/engine/audio_graph_test.cpp\n    unittests/engine/track_test.cpp\n    unittests/engine/engine_test.cpp\n    unittests/engine/parameter_manager_test.cpp\n    unittests/engine/processor_container_test.cpp\n    unittests/engine/midi_dispatcher_test.cpp\n    unittests/engine/json_configurator_test.cpp\n    unittests/engine/receiver_test.cpp\n    unittests/engine/event_dispatcher_test.cpp\n    unittests/engine/event_timer_test.cpp\n    unittests/engine/transport_test.cpp\n    unittests/engine/controller_test.cpp\n    unittests/engine/plugin_library_test.cpp\n    unittests/engine/controllers/audio_graph_controller_test.cpp\n    unittests/engine/controllers/audio_routing_controller_test.cpp\n    unittests/engine/controllers/osc_controller_test.cpp\n    unittests/engine/controllers/midi_controller_test.cpp\n    unittests/engine/controllers/session_controller_test.cpp\n    unittests/engine/controllers/reactive_controller_test.cpp\n    unittests/engine/factories/factories_test.cpp\n    unittests/audio_frontends/offline_frontend_test.cpp\n    unittests/control_frontends/osc_frontend_test.cpp\n    unittests/control_frontends/oscpack_osc_messenger_test.cpp\n    unittests/dsp_library/envelope_test.cpp\n    unittests/dsp_library/master_limiter_test.cpp\n    unittests/dsp_library/sample_wrapper_test.cpp\n    unittests/dsp_library/value_smoother_test.cpp\n    unittests/library/event_test.cpp\n    unittests/library/processor_test.cpp\n    unittests/library/sample_buffer_test.cpp\n    unittests/library/midi_decoder_test.cpp\n    unittests/library/midi_encoder_test.cpp\n    unittests/library/parameter_dump_test.cpp\n    unittests/library/performance_timer_test.cpp\n    unittests/library/plugin_parameters_test.cpp\n    unittests/library/internal_plugin_test.cpp\n    unittests/library/rt_event_test.cpp\n    unittests/library/id_generator_test.cpp\n    unittests/library/simple_fifo_test.cpp\n    unittests/library/fixed_stack_test.cpp\n)\n\nset(TEST_HELPER_FILES ${TEST_HELPER_FILES}\n    ${PROJECT_SOURCE_DIR}/src/library/processor_state.cpp\n    ${PROJECT_SOURCE_DIR}/src/audio_frontends/base_audio_frontend.cpp\n    ${PROJECT_SOURCE_DIR}/src/plugins/transposer_plugin.cpp\n    ${PROJECT_SOURCE_DIR}/src/library/lv2/lv2_processor_factory.cpp\n    ${PROJECT_SOURCE_DIR}/src/library/vst2x/vst2x_processor_factory.cpp\n    ${PROJECT_SOURCE_DIR}/src/library/vst3x/vst3x_processor_factory.cpp\n    ${PROJECT_SOURCE_DIR}/test/unittests/test_utils/portaudio_mockup.cpp\n    ${PROJECT_SOURCE_DIR}/test/unittests/test_utils/vst3_test_plugin.cpp\n    ${FREEVERB_SOURCES}\n)\n\nset(INCLUDE_DIRS ${INCLUDE_DIRS}\n    ${PROJECT_SOURCE_DIR}/test/unittests\n    ${PROJECT_SOURCE_DIR}/test/gtest/include\n    ${PROJECT_SOURCE_DIR}/third-party/portaudio/include\n)\n\nset(TEST_LINK_LIBRARIES\n    ${COMMON_LIBRARIES}\n    gtest\n    gtest_main\n    gmock\n    gmock_main\n)\n\nset(TEST_COMPILE_DEFINITIONS\n    -DELKLOG_DISABLE_LOGGING\n    -D__cdecl=\n    -DSUSHI_CUSTOM_AUDIO_CHUNK_SIZE=${SUSHI_AUDIO_BUFFER_SIZE}\n)\n\nset(TEST_DEPENDENCIES \"\")\n\n\n######################\n# Build options      #\n######################\n\nif (APPLE)\n    set(TEST_COMPILE_DEFINITIONS ${TEST_COMPILE_DEFINITIONS} -DSUSHI_APPLE_THREADING)\nendif()\n\n# Build Test plugin, needed for dynamic library loading unit tests\n# Adapted from https://github.com/gmoe/vst-cmake\n\nif (${SUSHI_WITH_VST2})\n    set(VST2_SDK_PLUGIN_SOURCES\n            \"${SUSHI_VST2_SDK_PATH}/public.sdk/source/vst2.x/audioeffectx.cpp\"\n            \"${SUSHI_VST2_SDK_PATH}/public.sdk/source/vst2.x/audioeffect.cpp\"\n            \"${SUSHI_VST2_SDK_PATH}/public.sdk/source/vst2.x/vstplugmain.cpp\"\n            \"${SUSHI_VST2_SDK_PATH}/pluginterfaces/vst2.x/aeffectx.h\"\n    )\n\n    set(VST2_TEST_PLUGIN_SOURCES\n            unittests/test_utils/vst2_test_plugin.h\n            unittests/test_utils/vst2_test_plugin.cpp\n            ${VST2_SDK_PLUGIN_SOURCES}\n    )\n    if (MSVC)\n        set(VST2_TEST_PLUGIN_SOURCES\n                unittests/test_utils/vst2_test_plugin.def\n                ${VST2_TEST_PLUGIN_SOURCES}\n        )\n    endif ()\n\n    add_library(vst2_test_plugin MODULE ${VST2_TEST_PLUGIN_SOURCES})\n\n    # Suppress VST SDK warnings\n    set(VST2_COMPILE_FLAS -Wall -Wno-write-strings -Wno-narrowing)\n    if (CMAKE_CXX_COMPILER_ID STREQUAL \"GNU\")\n        set(VST2_COMPILE_FLAS ${VST2_COMPILE_FLAGS} -Wno-stringop-truncation)\n    endif()\n    set_target_properties(vst2_test_plugin PROPERTIES\n            COMPILE_FLAGS \"${VST2_COMPILE_FLAGS}\"\n    )\n    target_compile_features(vst2_test_plugin PUBLIC cxx_std_20)\n    target_include_directories(vst2_test_plugin PRIVATE ${SUSHI_VST2_SDK_PATH})\n    target_compile_definitions(vst2_test_plugin PRIVATE -D__cdecl= )\n    if (${CMAKE_SYSTEM_NAME} STREQUAL \"Darwin\")\n        set_target_properties(vst2_test_plugin PROPERTIES\n            BUNDLE true\n            BUNDLE_EXTENSION \"vst\"\n            XCODE_ATTRIBUTE_WRAPPER_EXTENSION \"vst\"\n            MACOSX_BUNDLE_BUNDLE_NAME \"TestPlugin\"\n            MACOSX_BUNDLE_GUI_IDENTIFIER \"com.ELK.TestPlugin\"\n            MACOSX_BUNDLE_ICON_FILE \"\"\n            MACOSX_BUNDLE_SHORT_VERSION_STRING \"1.0.0\"\n            MACOSX_BUNDLE_COPYRIGHT \"ELK © 2016\"\n    )\n    endif()\n\n    set(TEST_FILES ${TEST_FILES}\n        unittests/library/vst2x_wrapper_test.cpp\n        unittests/library/vst2x_plugin_loading_test.cpp\n        unittests/library/vst2x_midi_event_fifo_test.cpp\n        ${PROJECT_SOURCE_DIR}/src/library/vst2x/vst2x_host_callback.cpp\n    )\n\n    set(TEST_COMPILE_DEFINITIONS ${TEST_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_VST2)\n    if (APPLE)\n        set(TEST_COMPILE_DEFINITIONS ${TEST_COMPILE_DEFINITIONS} -DVST2_TEST_PLUGIN_PATH=\"$<TARGET_BUNDLE_DIR:vst2_test_plugin>\")\n        set(TEST_LINK_LIBRARIES ${TEST_LINK_LIBRARIES} \"-framework CoreFoundation\")\n    else()\n        set(TEST_COMPILE_DEFINITIONS ${TEST_COMPILE_DEFINITIONS} -DVST2_TEST_PLUGIN_PATH=\"$<TARGET_FILE:vst2_test_plugin>\")\n    endif()\n    set(TEST_DEPENDENCIES ${TEST_DEPENDENCIES} vst2_test_plugin)\nendif()\n\nif (${SUSHI_WITH_JACK})\n    set(TEST_FILES ${TEST_FILES} unittests/audio_frontends/jack_frontend_test.cpp)\n    set(TEST_COMPILE_DEFINITIONS ${TEST_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_JACK)\nendif()\n\nif (${SUSHI_WITH_PORTAUDIO})\n    set(TEST_FILES ${TEST_FILES} unittests/audio_frontends/portaudio_frontend_test.cpp)\n    set(TEST_COMPILE_DEFINITIONS ${TEST_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_PORTAUDIO)\nendif()\n\nif (${SUSHI_WITH_APPLE_COREAUDIO})\n    set(TEST_FILES ${TEST_FILES}\n            unittests/audio_frontends/apple_coreaudio_frontend_test.cpp\n            unittests/test_utils/apple_coreaudio_mockup.cpp\n            ../src/audio_frontends/apple_coreaudio/apple_coreaudio_object.cpp\n            ../src/audio_frontends/apple_coreaudio/apple_coreaudio_device.mm\n            ../src/audio_frontends/apple_coreaudio/apple_coreaudio_utils.cpp)\n    set(TEST_COMPILE_DEFINITIONS ${TEST_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_APPLE_COREAUDIO)\n    set(TEST_LINK_LIBRARIES ${TEST_LINK_LIBRARIES} \"-framework CoreAudio -framework Foundation\")\nendif()\n\nif (${SUSHI_WITH_VST3})\n    set(TEST_FILES ${TEST_FILES} unittests/library/vst3x_wrapper_test.cpp)\n    set(TEST_COMPILE_DEFINITIONS ${TEST_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_VST3)\n    if (APPLE)\n        # When building with Xcode the result of TARGET_BUNDLE_DIR returns .bundle instead of .vst3 so we need to replace it\n        set(TEST_COMPILE_DEFINITIONS ${TEST_COMPILE_DEFINITIONS} -DSUSHI_VST3_TEST_PLUGIN_PATH=\"$<PATH:REPLACE_EXTENSION,$<TARGET_BUNDLE_DIR:adelay>,.vst3>\")\n    else()\n        set(TEST_COMPILE_DEFINITIONS ${TEST_COMPILE_DEFINITIONS} -DSUSHI_VST3_TEST_PLUGIN_PATH=\"../VST3/$<IF:$<CONFIG:DEBUG>,Debug,Release>/adelay.vst3\")\n    endif()\n    set(TEST_LINK_LIBRARIES ${TEST_LINK_LIBRARIES} vst3_host sdk base elk_vst3_extensions)\n    set(TEST_DEPENDENCIES ${TEST_DEPENDENCIES} adelay vst3_host)\nendif()\n\nif (${SUSHI_WITH_LV2})\n    set(TEST_FILES ${TEST_FILES} unittests/library/lv2_wrapper_test.cpp)\n    set(TEST_COMPILE_DEFINITIONS ${TEST_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_LV2)\n    set(TEST_LINK_LIBRARIES ${TEST_LINK_LIBRARIES} PkgConfig::LILV lv2_host)\n    set(TEST_DEPENDENCIES ${TEST_DEPENDENCIES} lv2_host)\n\n    include(ExternalProject)\n\n    ExternalProject_Add(lv2_examples\n        GIT_REPOSITORY https://github.com/lv2/lv2-examples.git\n        GIT_TAG a89d5691465a065146ecf14737a00fa0ddefd25c\n        PREFIX ${CMAKE_BINARY_DIR}/lv2_examples\n        CONFIGURE_COMMAND meson setup <BINARY_DIR> <SOURCE_DIR>\n                            --prefix=/\n                            -Dlv2dir=lv2\n        BUILD_COMMAND meson compile -C <BINARY_DIR>\n        INSTALL_COMMAND meson install -C <BINARY_DIR> --destdir <INSTALL_DIR>\n    )\nendif()\n\nif (${SUSHI_WITH_LV2_MDA_TESTS})\n    set(TEST_COMPILE_DEFINITIONS ${TEST_COMPILE_DEFINITIONS} -DSUSHI_BUILD_WITH_LV2_MDA_TESTS)\nendif()\n\nif (${SUSHI_WITH_ALSA_MIDI})\n    find_library(ALSA_LIB NAMES asound)\n    set(TEST_LINK_LIBRARIES ${TEST_LINK_LIBRARIES} ${ALSA_LIB})\nendif()\n\n###############################\n#  Test target configuration  #\n###############################\nadd_executable(unit_tests ${TEST_FILES} ${TEST_HELPER_FILES})\n\ntarget_compile_definitions(unit_tests PRIVATE ${TEST_COMPILE_DEFINITIONS})\n\nif(MSVC)\n    # C5045: Compiler will insert Spectre mitigation for memory load if /Qspectre switch specified\n    #        is disabled for all - Spectre is not a concern for Sushi I don't think.\n    # C4996: Deprecated warning. This was flooding the terminal, I might remove the suppression from here eventually,\n    #        and out into the code again.\n    target_compile_options(unit_tests PRIVATE /W3 /wd5045 /wd4996)\nelse ()\n    target_compile_options(unit_tests PRIVATE -Wall -fno-rtti -ffast-math)\nendif()\n\ntarget_include_directories(unit_tests\n        PUBLIC\n            $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>\n            ${INCLUDE_DIRS}\n            ${PUBLIC_INCLUDE_DIRS})\n\ntarget_link_libraries(unit_tests \"${TEST_LINK_LIBRARIES}\")\nif (TEST_DEPENDENCIES)\n    add_dependencies(unit_tests ${TEST_DEPENDENCIES})\nendif()\n\n# Disable Link in tests as this slows down the testing a lot due to network activity etc\nremove_definitions(-DSUSHI_BUILD_WITH_ABLETON_LINK)\n\n\nif(${SUSHI_DISABLE_MULTICORE_UNIT_TESTS})\n    target_compile_definitions(unit_tests PRIVATE -DDISABLE_MULTICORE_UNIT_TESTS)\nendif()\n\nadd_test(unit_tests unit_tests)\n\n### Custom command to copy the dynamic dependencies to the binary folder\n#   Mainly for twine.dll because windows cannot find it from ../twine but\n#   needs it in the same directory\nif (MSVC)\n    add_custom_command(TARGET unit_tests POST_BUILD\n    COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_RUNTIME_DLLS:unit_tests> $<TARGET_FILE_DIR:unit_tests>\n    COMMAND_EXPAND_LISTS)\nendif()\n\n### Custom target for running the tests\n# Environment variable pointing to test/data/ is set so that\n# tests can read it to access data files maintaining an independent out-of-source build\n\n# Execute tests if we are not cross-compiling\nif(CMAKE_CROSSCOMPILING)\n    message(STATUS \"Detected cross-compiling: skipping unit tests run.\")\nelse()\n    add_custom_target(run_tests ALL\n                      ${CMAKE_COMMAND}\n                      -E env\n                        \"SUSHI_TEST_DATA_DIR=${PROJECT_SOURCE_DIR}/test/data\"\n                        \"LV2_PATH=${PROJECT_BINARY_DIR}/lv2_examples/lv2\"\n                      \"$<TARGET_FILE:unit_tests>\")\n    add_dependencies(run_tests unit_tests)\nendif()\n\n"
  },
  {
    "path": "test/data/config.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000,\n        \"tempo\" : 100,\n        \"time_signature\" :\n        {\n            \"numerator\" : 4,\n            \"denominator\": 4\n        },\n        \"playing_mode\" : \"playing\",\n        \"tempo_sync\" : \"internal\",\n        \"cv_inputs\" : 1,\n        \"cv_outputs\" : 2,\n        \"audio_clip_detection\" :\n        {\n            \"inputs\" : false,\n            \"outputs\" : true\n        }\n    },\n    \"post_track\" :\n    {\n        \"name\" : \"master\",\n        \"plugins\": []\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.passthrough\",\n                    \"name\" : \"passthrough_0_l\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.testing.gain\",\n                    \"name\" : \"gain_0_l\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.testing.equalizer\",\n                    \"name\" : \"equalizer_0_l\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        },\n        {\n            \"name\" : \"monotrack\",\n            \"channels\" : 1,\n            \"inputs\" : [\n                {\n                    \"engine_channel\" : 2,\n                    \"track_channel\" : 0\n                }\n            ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 1,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.gain\",\n                    \"name\" : \"gain_0_r\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.testing.passthrough\",\n                    \"name\" : \"passthrough_0_r\",\n                    \"type\" : \"internal\"\n                },\n                {\n                    \"uid\" : \"sushi.testing.gain\",\n                    \"name\" : \"gain_1_r\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        },\n        {\n            \"name\" : \"monobustrack\",\n            \"channels\" : 1,\n            \"inputs\" : [\n                {\n                    \"engine_channel\" : 2,\n                    \"track_channel\" : 0\n                }\n            ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 3,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n            ]\n        },\n        {\n            \"name\" : \"multi\",\n            \"multibus\" : true,\n            \"buses\" : 2,\n            \"inputs\" : [],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 1,\n                    \"track_bus\" : 1\n                }\n            ],\n            \"plugins\" : []\n        }\n    ],\n    \"events\" : [\n        {\n            \"time\" : 0.9,\n            \"type\" : \"parameter_change\",\n            \"data\" : {\n                \"plugin_name\" : \"gain_0_r\",\n                \"parameter_name\" : \"gain\",\n                \"value\" : -23.0\n            }\n        },\n        {\n            \"time\" : 0.1,\n            \"type\" : \"parameter_change\",\n            \"data\" : {\n                \"plugin_name\" : \"gain_0_l\",\n                \"parameter_name\" : \"gain\",\n                \"value\" : -12.0\n            }\n        },\n        {\n            \"time\" : 0.5,\n            \"type\" : \"parameter_change\",\n            \"data\" : {\n                \"plugin_name\" : \"gain_0_r\",\n                \"parameter_name\" : \"gain\",\n                \"value\" : -6.0\n            }\n        },\n        {\n            \"time\" : 0.85,\n            \"type\" : \"parameter_change\",\n            \"data\" : {\n                \"plugin_name\" : \"gain_0_l\",\n                \"parameter_name\" : \"gain\",\n                \"value\" : 0.0\n            }\n        }\n\n    ],\n    \"midi\" : {\n        \"track_connections\": [\n            {\n                \"port\": 0,\n                \"channel\": \"all\",\n                \"track\": \"main\",\n                \"raw_midi\": false\n            },\n            {\n                \"port\": 0,\n                \"channel\": 10,\n                \"track\": \"monotrack\",\n                \"raw_midi\": true\n            }\n        ],\n        \"program_change_connections\": [\n            {\n                \"port\": 0,\n                \"channel\": \"all\",\n                \"plugin\": \"gain_0_r\"\n            }\n        ],\n        \"cc_mappings\": [\n            {\n                \"port\": 0,\n                \"channel\": 0,\n                \"cc_number\": 27,\n                \"plugin_name\": \"equalizer_0_l\",\n                \"parameter_name\": \"gain\",\n                \"min_range\": -24,\n                \"max_range\": 24\n            }\n        ],\n        \"clock_output\": {\n            \"enabled_ports\" : [0]\n        }\n    },\n    \"osc\" : {\n        \"enabled_processor_outputs\" : [\n            {\n                \"processor\" : \"gain_0_l\"\n            }\n        ]\n    },\n    \"cv_control\" : {\n        \"cv_inputs\" : [\n            {\n                \"cv\" : 0,\n                \"processor\" : \"gain_0_l\",\n                \"parameter\" : \"gain\"\n            },\n            {\n                \"cv\" : 1,\n                \"processor\" : \"synth\",\n                \"parameter\" : \"cutoff\"\n            }\n        ],\n        \"cv_outputs\" : [\n            {\n                \"cv\" : 0,\n                \"processor\" : \"gain_0_r\",\n                \"parameter\" : \"gain\"\n            }\n        ],\n        \"gate_inputs\" : [\n            {\n                \"gate\" : 0,\n                \"mode\" : \"note_event\",\n                \"note_no\" : 20,\n                \"processor\" : \"synth\",\n                \"channel\" : 0\n            },\n            {\n                \"gate\" : 1,\n                \"mode\" : \"sync\",\n                \"ppq_ticks\" : 2\n            }\n        ],\n        \"gate_outputs\" : [\n            {\n                \"gate\" : 0,\n                \"mode\" : \"note_event\",\n                \"note_no\" : 0,\n                \"processor\" : \"seq\",\n                \"channel\" : 0\n            }\n        ]\n    },\n    \"initial_state\" :\n    [\n        {\n            \"processor\" : \"main\",\n            \"program\" : 24,\n            \"bypassed\" : false,\n            \"parameters\" : {\n                \"pan\" : 0.35,\n                \"gain\" : 0.60\n            }\n        },\n        {\n            \"processor\" : \"gain_0_l\",\n            \"program\" : 0,\n            \"parameters\" : {\n                \"gain\" : 0.60\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "test/data/config_single_stereo.json",
    "content": "{\n    \"host_config\" : {\n        \"samplerate\" : 48000\n    },\n    \"tracks\" : [\n        {\n            \"name\" : \"main\",\n            \"channels\" : 2,\n            \"inputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"outputs\" : [\n                {\n                    \"engine_bus\" : 0,\n                    \"track_bus\" : 0\n                }\n            ],\n            \"plugins\" : [\n                {\n                    \"uid\" : \"sushi.testing.passthrough\",\n                    \"name\" : \"passthrough\",\n                    \"type\" : \"internal\"\n                }\n            ]\n        }\n    ],\n    \"midi\" : {\n        \"cc_mappings\" : [\n        ]\n    }\n}\n"
  },
  {
    "path": "test/data/master_limiter_test_data.h",
    "content": "#ifndef SUSHI_MASTER_LIMITER_TEST_DATA_H\n#define SUSHI_MASTER_LIMITER_TEST_DATA_H\n//This data was generated from the python script used to design the algorithm\nconstexpr int UPSAMPLING_TEST_DATA_SIZE = 16;\nconstexpr float UPSAMPLING_TEST_DATA[UPSAMPLING_TEST_DATA_SIZE] = {\n    1.6031981706619263f, 0.9191625118255615f, 0.9441951513290405f, 0.497483491897583f,\n    0.4227568805217743f, 0.02524338662624359f, -0.07185277342796326f, -0.4660314917564392f,\n    -0.5481231212615967f, -0.9815558791160583f, -0.9807688593864441f, -1.6091821193695068f,\n    0.47370654344558716f, 1.5271077156066895f, 0.9095961451530457f, 0.8903854489326477\n};\n\nconstexpr int UPSAMPLING_TEST_DATA4X_SIZE = 64;\nconstexpr float UPSAMPLING_TEST_DATA4X[UPSAMPLING_TEST_DATA4X_SIZE] = {\n    -0.1060667410492897f, -0.14819470047950745f, -0.133992999792099f, -2.7093278731626924e-08f,\n    0.2476349174976349f, 0.6839537024497986f, 1.1730483770370483f, 1.5605404376983643f,\n    1.6749145984649658f, 1.6034364700317383f, 1.406592845916748f, 1.2031517028808594f,\n    1.0434499979019165f, 0.9894606471061707f, 0.987175464630127f, 0.9898470044136047f,\n    0.9868156909942627f, 0.8588047623634338f, 0.7203959226608276f, 0.6050930023193359f,\n    0.5639124512672424f, 0.5093562602996826f, 0.4787982106208801f, 0.4447537660598755f,\n    0.4211186170578003f, 0.30675584077835083f, 0.18246017396450043f, 0.07299449294805527f,\n    0.04158003628253937f, -0.007036561146378517f, -0.044038183987140656f, -0.09305360913276672f,\n    -0.12333919107913971f, -0.23097600042819977f, -0.3543069362640381f, -0.46912553906440735f,\n    -0.494147926568985f, -0.5294740200042725f, -0.5621595978736877f, -0.6184467673301697f,\n    -0.6574973464012146f, -0.7684823870658875f, -0.9030699729919434f, -1.030062198638916f,\n    -1.0376704931259155f, -1.0410641431808472f, -1.050227165222168f, -1.1072548627853394f,\n    -1.2956103086471558f, -1.498157262802124f, -1.6737871170043945f, -1.6901203393936157f,\n    -1.5762592554092407f, -1.0865287780761719f, -0.43945568799972534f, 0.21639184653759003f,\n    0.6947306394577026f, 1.1521437168121338f, 1.490471601486206f, 1.6840757131576538f,\n    1.60256826877594f, 1.5049097537994385f, 1.323349952697754f, 1.1478605270385742\n};\n\nconstexpr int LIMITER_INPUT_DATA_SIZE = 256;\nconstexpr float LIMITER_INPUT_DATA[LIMITER_INPUT_DATA_SIZE] = {\n    0.00001f, 0.15613864362239838f, 0.3103301227092743f, 0.4624886214733124f,\n    0.6125308871269226f, 0.7603762149810791f, 0.9059464931488037f, 1.0491665601730347f,\n    1.189963698387146f, 1.3282678127288818f, 1.4640123844146729f, 1.5971318483352661f,\n    1.727565884590149f, 1.8552534580230713f, 1.980139970779419f, 2.1021716594696045f,\n    2.2212975025177f, 2.3374688625335693f, 2.450641393661499f, 2.5607717037200928f,\n    2.667820453643799f, 2.7717506885528564f, 2.8725273609161377f, 2.9701199531555176f,\n    3.064497947692871f, 3.155634641647339f, 3.2435085773468018f, 3.3280959129333496f,\n    3.4093799591064453f, 3.4873440265655518f, 3.56197452545166f, 3.633260488510132f,\n    3.7011938095092773f, 3.7657692432403564f, 3.826982021331787f, 3.8848307132720947f,\n    3.9393177032470703f, 3.990446090698242f, 4.038221836090088f, 4.082652568817139f,\n    4.123749732971191f, 4.161525249481201f, 4.195992469787598f, 4.227170944213867f,\n    4.255077838897705f, 4.279734134674072f, 4.3011627197265625f, 4.319388389587402f,\n    4.334438323974609f, 4.346340656280518f, 4.35512638092041f, 4.36082649230957f,\n    4.363475322723389f, 4.363108158111572f, 4.359763145446777f, 4.3534770011901855f,\n    4.344291687011719f, 4.332247734069824f, 4.317388534545898f, 4.2997589111328125f,\n    4.2794036865234375f, 4.256371021270752f, 4.230708122253418f, 4.202465534210205f,\n    4.171691417694092f, 4.1384406089782715f, 4.102763652801514f, 4.064713954925537f,\n    4.024346351623535f, 3.981717109680176f, 3.9368817806243896f, 3.8898963928222656f,\n    3.840820550918579f, 3.789710521697998f, 3.736626386642456f, 3.6816279888153076f,\n    3.62477445602417f, 3.5661263465881348f, 3.505744695663452f, 3.443692207336426f,\n    3.38002872467041f, 3.31481671333313f, 3.248119592666626f, 3.179997444152832f,\n    3.110513687133789f, 3.0397326946258545f, 2.967714786529541f, 2.894524097442627f,\n    2.8202216625213623f, 2.744873285293579f, 2.6685378551483154f, 2.591279983520508f,\n    2.513162136077881f, 2.4342451095581055f, 2.3545901775360107f, 2.274259567260742f,\n    2.19331431388855f, 2.111816883087158f, 2.02982497215271f, 1.9473986625671387f,\n    1.8645967245101929f, 1.7814819812774658f, 1.6981080770492554f, 1.6145360469818115f,\n    1.5308219194412231f, 1.4470198154449463f, 1.3631895780563354f, 1.2793846130371094f,\n    1.195658564567566f, 1.1120662689208984f, 1.028661847114563f, 0.9454929232597351f,\n    0.8626155257225037f, 0.7800771594047546f, 0.6979296803474426f, 0.6162210702896118f,\n    0.5349977612495422f, 0.4543096125125885f, 0.37420040369033813f, 0.2947145104408264f,\n    0.21589824557304382f, 0.13779540359973907f, 0.060443658381700516f, -0.016111472621560097f,\n    -0.09183213859796524f, -0.16668003797531128f, -0.24061359465122223f, -0.31360021233558655f,\n    -0.3856026232242584f, -0.4565853476524353f, -0.5265141129493713f, -0.5953575372695923f,\n    -0.6630842685699463f, -0.7296633124351501f, -0.7950667142868042f, -0.8592634797096252f,\n    -0.9222308397293091f, -0.9839386343955994f, -1.04436194896698f, -1.1034841537475586f,\n    -1.1612735986709595f, -1.2177144289016724f, -1.2727832794189453f, -1.3264650106430054f,\n    -1.3787368535995483f, -1.429585337638855f, -1.4789925813674927f, -1.5269465446472168f,\n    -1.5734301805496216f, -1.618432879447937f, -1.6619406938552856f, -1.7039470672607422f,\n    -1.7444413900375366f, -1.7834134101867676f, -1.8208578824996948f, -1.8567681312561035f,\n    -1.8911381959915161f, -1.9239658117294312f, -1.9552459716796875f, -1.984978437423706f,\n    -2.013160228729248f, -2.039792060852051f, -2.0648751258850098f, -2.0884110927581787f,\n    -2.1104023456573486f, -2.130852460861206f, -2.1497645378112793f, -2.167147636413574f,\n    -2.1830053329467773f, -2.1973447799682617f, -2.210175037384033f, -2.2215051651000977f,\n    -2.2313437461853027f, -2.239701271057129f, -2.2465901374816895f, -2.2520217895507812f,\n    -2.2560083866119385f, -2.2585644721984863f, -2.2597031593322754f, -2.2594399452209473f,\n    -2.2577905654907227f, -2.254770517349243f, -2.250397205352783f, -2.2446885108947754f,\n    -2.237661123275757f, -2.2293355464935303f, -2.21972918510437f, -2.2088630199432373f,\n    -2.1967573165893555f, -2.18343186378479f, -2.1689095497131348f, -2.1532108783721924f,\n    -2.1363580226898193f, -2.1183743476867676f, -2.099282741546631f, -2.079106569290161f,\n    -2.0578689575195312f, -2.035595417022705f, -2.012308359146118f, -1.9880355596542358f,\n    -1.9627999067306519f, -1.936627984046936f, -1.909544587135315f, -1.8815752267837524f,\n    -1.8527469635009766f, -1.823087215423584f, -1.792620062828064f, -1.7613744735717773f,\n    -1.7293751239776611f, -1.6966488361358643f, -1.6632252931594849f, -1.6291309595108032f,\n    -1.5943913459777832f, -1.5590351819992065f, -1.523087739944458f, -1.4865803718566895f,\n    -1.4495381116867065f, -1.4119869470596313f, -1.373957872390747f, -1.3354754447937012f,\n    -1.296567440032959f, -1.2572606801986694f, -1.2175852060317993f, -1.177565097808838f,\n    -1.1372281312942505f, -1.0966001749038696f, -1.0557104349136353f, -1.0145822763442993f,\n    -0.9732460975646973f, -0.9317242503166199f, -0.8900450468063354f, -0.8482334613800049f,\n    -0.80631422996521f, -0.7643154263496399f, -0.7222611904144287f, -0.6801739931106567f,\n    -0.6380810737609863f, -0.5960057377815247f, -0.5539714694023132f, -0.5120035409927368f,\n    -0.47012558579444885f, -0.42835816740989685f, -0.3867282271385193f, -0.34525439143180847f,\n    -0.3039618730545044f, -0.2628702223300934f, -0.222000852227211f, -0.18137653172016144f,\n    -0.14101718366146088f, -0.10094314068555832f, -0.06117207929491997f, -0.021730655804276466f,\n    0.01736903376877308f, 0.0561058335006237f, 0.0944613590836525f, 0.13241761922836304\n};\n\nconstexpr int LIMITER_OUTPUT_DATA_SIZE = 256;\nconstexpr float LIMITER_OUTPUT_DATA[LIMITER_OUTPUT_DATA_SIZE] = {\n    0.00001f, 0.15613864362239838f, 0.3103301227092743f, 0.4624886214733124f,\n    0.6125308871269226f, 0.7603762149810791f, 0.9059464931488037f, 1.0491337776184082f,\n    1.1897999048233032f, 1.3278663158416748f, 1.4632607698440552f, 1.595912218093872f,\n    1.7257542610168457f, 1.8527257442474365f, 1.9767727851867676f, 2.0978429317474365f,\n    2.2158873081207275f, 2.330859661102295f, 2.442718982696533f, 2.551424741744995f,\n    2.6569416522979736f, 2.7592363357543945f, 2.858278274536133f, 2.954041004180908f,\n    3.046499013900757f, 3.135629892349243f, 3.2214174270629883f, 3.303842782974243f,\n    3.3828940391540527f, 3.4585602283477783f, 3.530832529067993f, 3.599705934524536f,\n    3.6651771068573f, 3.7272467613220215f, 3.7859153747558594f, 3.8411872386932373f,\n    3.8930699825286865f, 3.9415721893310547f, 3.986705780029297f, 4.0284833908081055f,\n    4.066921710968018f, 4.102038860321045f, 4.133853435516357f, 4.162389278411865f,\n    4.1876702308654785f, 4.209721088409424f, 4.228570938110352f, 4.24424934387207f,\n    4.25678825378418f, 4.266220569610596f, 4.272582530975342f, 4.275909423828125f,\n    4.276240348815918f, 4.273614883422852f, 4.268075466156006f, 4.259662628173828f,\n    4.248422145843506f, 4.234397888183594f, 4.217637062072754f, 4.198187351226807f,\n    4.176097393035889f, 4.1514177322387695f, 4.124198913574219f, 4.094493865966797f,\n    4.06235408782959f, 4.027836322784424f, 3.9909939765930176f, 3.9518823623657227f,\n    3.9105584621429443f, 3.8670806884765625f, 3.8215062618255615f, 3.773892879486084f,\n    3.724301815032959f, 3.67279052734375f, 3.6194207668304443f, 3.564253091812134f,\n    3.507347822189331f, 3.448765993118286f, 3.3885700702667236f, 3.326822519302368f,\n    3.2635838985443115f, 3.1989171504974365f, 3.132885694503784f, 3.065549373626709f,\n    2.996971845626831f, 2.9272170066833496f, 2.856344699859619f, 2.7844185829162598f,\n    2.7114992141723633f, 2.6376514434814453f, 2.5629334449768066f, 2.487408399581909f,\n    2.411137819290161f, 2.334181308746338f, 2.256598711013794f, 2.1784508228302f,\n    2.099796772003174f, 2.020697593688965f, 1.941208839416504f, 1.861388921737671f,\n    1.781294584274292f, 1.7009862661361694f, 1.6205155849456787f, 1.5399411916732788f,\n    1.459316611289978f, 1.3786935806274414f, 1.2981292009353638f, 1.2176743745803833f,\n    1.1373800039291382f, 1.057297945022583f, 0.9774795174598694f, 0.8979694843292236f,\n    0.8188207149505615f, 0.7400776743888855f, 0.6617891192436218f, 0.583999752998352f,\n    0.5067528486251831f, 0.4300948679447174f, 0.3540663719177246f, 0.2787083685398102f,\n    0.20406363904476166f, 0.1301724910736084f, 0.057069387286901474f, -0.015203922986984253f,\n    -0.0866129919886589f, -0.15712301433086395f, -0.2266962081193924f, -0.2953032851219177f,\n    -0.362910658121109f, -0.4294864535331726f, -0.49500009417533875f, -0.5594236850738525f,\n    -0.622729480266571f, -0.68489009141922f, -0.7458810210227966f, -0.8056751489639282f,\n    -0.8642529249191284f, -0.9215879440307617f, -0.9776588678359985f, -1.03245210647583f,\n    -1.0859400033950806f, -1.138109803199768f, -1.1889418363571167f, -1.2384239435195923f,\n    -1.2865370512008667f, -1.3332706689834595f, -1.378610372543335f, -1.4225472211837769f,\n    -1.4650672674179077f, -1.5061631202697754f, -1.5458240509033203f, -1.5840460062026978f,\n    -1.6208215951919556f, -1.6561435461044312f, -1.690009355545044f, -1.7224149703979492f,\n    -1.7533572912216187f, -1.7828364372253418f, -1.8108502626419067f, -1.8374006748199463f,\n    -1.8624874353408813f, -1.8861134052276611f, -1.908281922340393f, -1.9289971590042114f,\n    -1.94826340675354f, -1.9660862684249878f, -1.982470989227295f, -1.9974281787872314f,\n    -2.0109634399414062f, -2.023085594177246f, -2.0338051319122314f, -2.043132781982422f,\n    -2.0510787963867188f, -2.057654857635498f, -2.0628745555877686f, -2.0667505264282227f,\n    -2.06929612159729f, -2.0705268383026123f, -2.0704567432403564f, -2.0691022872924805f,\n    -2.0664799213409424f, -2.062605619430542f, -2.057497501373291f, -2.0511741638183594f,\n    -2.043652296066284f, -2.0349528789520264f, -2.0250935554504395f, -2.0140953063964844f,\n    -2.001978635787964f, -1.9887633323669434f, -1.9744720458984375f, -1.95912504196167f,\n    -1.942744255065918f, -1.925352692604065f, -1.9069727659225464f, -1.8876272439956665f,\n    -1.86733877658844f, -1.8461319208145142f, -1.8240286111831665f, -1.8010554313659668f,\n    -1.7772345542907715f, -1.7525913715362549f, -1.7271498441696167f, -1.7009340524673462f,\n    -1.673970103263855f, -1.6462836265563965f, -1.6178977489471436f, -1.5888397693634033f,\n    -1.5591330528259277f, -1.528802752494812f, -1.497876763343811f, -1.4663797616958618f,\n    -1.4343358278274536f, -1.4017715454101562f, -1.3687106370925903f, -1.3351819515228271f,\n    -1.3012089729309082f, -1.2668155431747437f, -1.2320302724838257f, -1.196875810623169f,\n    -1.1613777875900269f, -1.1255606412887573f, -1.0894520282745361f, -1.053073763847351f,\n    -1.0164512395858765f, -0.9796079993247986f, -0.9425705671310425f, -0.9053600430488586f,\n    -0.8680039644241333f, -0.8305224776268005f, -0.7929410338401794f, -0.7552821040153503f,\n    -0.7175678610801697f, -0.6798233985900879f, -0.6420702338218689f, -0.6043285131454468f,\n    -0.566622257232666f, -0.528972327709198f, -0.491399347782135f, -0.4539257287979126f,\n    -0.416572242975235f, -0.3793570101261139f, -0.342303603887558f, -0.3054283559322357f,\n    -0.2687532603740692f, -0.23229534924030304f, -0.19607318937778473f, -0.16010653972625732f,\n    -0.12441261112689972f, -0.08900891989469528f, -0.05391061678528786f, -0.019140716642141342f,\n    0.01529062632471323f, 0.04936531186103821f, 0.08306771516799927f, 0.11638259142637253\n};\n\n#endif // !SUSHI_MASTER_LIMITER_TEST_DATA_H"
  },
  {
    "path": "test/resources/Info.plist.in",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>English</string>\n\t<key>CFBundleExecutable</key>\n\t<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>\n\t<key>CFBundleGetInfoString</key>\n\t<string>${MACOSX_BUNDLE_INFO_STRING}</string>\n\t<key>CFBundleIconFile</key>\n\t<string>${MACOSX_BUNDLE_ICON_FILE}</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleLongVersionString</key>\n\t<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>\n\t<key>CFBundleName</key>\n\t<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>\n\t<key>CFBundlePackageType</key>\n\t<string>BNDL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>\n\t<key>CSResourcesFileMapped</key>\n\t<true/>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>${MACOSX_BUNDLE_COPYRIGHT}</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "test/unittests/audio_frontends/apple_coreaudio_frontend_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#define protected public\n\n#include \"audio_frontends/apple_coreaudio/apple_coreaudio_system_object.h\"\n#include \"audio_frontends/apple_coreaudio/apple_coreaudio_utils.h\"\n\n#pragma clang diagnostic push\n#pragma clang diagnostic ignored \"-Wnullability-completeness\" // Ignore Apple nonsense\n#include \"../test_utils/apple_coreaudio_mockup.h\"\n#pragma clang diagnostic pop\n\n#include \"audio_frontends/apple_coreaudio/apple_coreaudio_object.h\"\n\n#include \"audio_frontends/apple_coreaudio_frontend.cpp\"\n\nusing ::testing::NiceMock;\nusing ::testing::Return;\n\nclass TestAppleCoreAudioFrontend : public ::testing::Test\n{\nprotected:\n    void SetUp() override\n    {\n        AppleAudioHardwareMockup::instance = &_mock;\n    }\n\n    void expect_calls_to_get_cf_string_property(AudioObjectID expected_audio_object_id, const AudioObjectPropertyAddress& expected_addr, const CFStringRef& cf_string_ref)\n    {\n        EXPECT_CALL(_mock, AudioObjectHasProperty)\n                .WillOnce(Return(true));\n        EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize)\n                .WillOnce([expected_audio_object_id, expected_addr](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* out_data_size) {\n                    EXPECT_EQ(audio_object_id, expected_audio_object_id);\n                    EXPECT_EQ(*address, expected_addr);\n                    *out_data_size = sizeof(CFStringRef);\n                    return kAudioHardwareNoError;\n                });\n        EXPECT_CALL(_mock, AudioObjectGetPropertyData)\n                .WillOnce([expected_audio_object_id, cf_string_ref, expected_addr](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* data_size, void* out_data) {\n                    EXPECT_EQ(audio_object_id, expected_audio_object_id);\n                    EXPECT_EQ(*address, expected_addr);\n                    *data_size = sizeof(CFStringRef);\n                    *static_cast<CFStringRef*>(out_data) = cf_string_ref;\n                    return kAudioHardwareNoError;\n                });\n    }\n\n    template<class DataType>\n    void expect_calls_to_set_property(AudioObjectID expected_audio_object_id, const AudioObjectPropertyAddress expected_address)\n    {\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n        EXPECT_CALL(_mock, AudioObjectIsPropertySettable).WillOnce([expected_audio_object_id, expected_address](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, Boolean* out_is_settable) {\n            EXPECT_EQ(audio_object_id, expected_audio_object_id);\n            EXPECT_EQ(*address, expected_address);\n            *out_is_settable = true;\n            return kAudioHardwareNoError;\n        });\n        EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([expected_audio_object_id, expected_address](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* out_data_size) {\n            EXPECT_EQ(audio_object_id, expected_audio_object_id);\n            EXPECT_EQ(*address, expected_address);\n            *out_data_size = sizeof(DataType);\n            return kAudioHardwareNoError;\n        });\n        EXPECT_CALL(_mock, AudioObjectSetPropertyData).WillOnce(Return(kAudioHardwareNoError));\n    }\n\n    template<class DataType>\n    void expect_calls_to_get_property(AudioObjectID expected_audio_object_id, const AudioObjectPropertyAddress expected_address, const DataType& return_value)\n    {\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n\n        EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([expected_audio_object_id, expected_address](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* out_data_size) {\n            EXPECT_EQ(audio_object_id, expected_audio_object_id);\n            EXPECT_EQ(*address, expected_address);\n            *out_data_size = sizeof(DataType);\n            return kAudioHardwareNoError;\n        });\n        EXPECT_CALL(_mock, AudioObjectGetPropertyData).WillOnce([return_value](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* data_size, void* out_data) {\n            *data_size = sizeof(DataType);\n            *static_cast<DataType*>(out_data) = return_value;\n            return kAudioHardwareNoError;\n        });\n    }\n\n    void expect_calls_for_getting_output_device_name(AudioObjectID expected_audio_object_id,\n                                                     const AudioObjectPropertyAddress& expected_address,\n                                                     const CFStringRef& cf_string_ref,\n                                                     const CFStringRef& return_value)\n    {\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillRepeatedly(Return(true));\n\n        EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* out_data_size) {\n                EXPECT_EQ(audio_object_id, kAudioObjectSystemObject);\n                AudioObjectPropertyAddress expected{kAudioHardwarePropertyDevices,\n                                                    kAudioObjectPropertyScopeGlobal,\n                                                    kAudioObjectPropertyElementMain};\n                EXPECT_EQ(*address, expected);\n                *out_data_size = 3 * sizeof(UInt32);\n                return kAudioHardwareNoError;\n            })\n            .WillOnce([expected_audio_object_id, &expected_address](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* out_data_size) {\n                EXPECT_EQ(audio_object_id, expected_audio_object_id);\n                EXPECT_EQ(*address, expected_address);\n                *out_data_size = sizeof(CFStringRef);\n                return kAudioHardwareNoError;\n            })\n            .WillOnce([expected_audio_object_id, &expected_address](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* out_data_size) {\n                EXPECT_EQ(audio_object_id, expected_audio_object_id);\n                EXPECT_EQ(*address, expected_address);\n                *out_data_size = sizeof(CFStringRef);\n                return kAudioHardwareNoError;\n            });\n\n        EXPECT_CALL(_mock, AudioObjectGetPropertyData).WillOnce([](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* data_size, void* out_data) {\n                EXPECT_EQ(audio_object_id, kAudioObjectSystemObject);\n                AudioObjectPropertyAddress expected{kAudioHardwarePropertyDevices,\n                                                    kAudioObjectPropertyScopeGlobal,\n                                                    kAudioObjectPropertyElementMain};\n                EXPECT_EQ(*address, expected);\n                *data_size = sizeof(UInt32);\n                static_cast<UInt32*>(out_data)[0] = 1;\n                return kAudioHardwareNoError;\n            })\n            .WillOnce([expected_audio_object_id, &cf_string_ref, &expected_address](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* data_size, void* out_data) {\n                EXPECT_EQ(audio_object_id, expected_audio_object_id);\n                EXPECT_EQ(*address, expected_address);\n                *data_size = sizeof(CFStringRef);\n                *static_cast<CFStringRef*>(out_data) = cf_string_ref;\n                return kAudioHardwareNoError;\n            })\n            .WillOnce([expected_audio_object_id, &return_value, &expected_address](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* data_size, void* out_data) {\n                EXPECT_EQ(audio_object_id, expected_audio_object_id);\n                EXPECT_EQ(*address, expected_address);\n                *data_size = sizeof(CFStringRef);\n                *static_cast<CFStringRef*>(out_data) = return_value;\n                return kAudioHardwareNoError;\n            });\n    }\n\n    testing::StrictMock<AppleAudioHardwareMockup> _mock;\n};\n\nTEST_F(TestAppleCoreAudioFrontend, audio_object_property_address_equality)\n{\n    AudioObjectPropertyAddress lhs{1, 2, 3};\n    AudioObjectPropertyAddress rhs{1, 2, 3};\n\n    EXPECT_EQ(lhs, rhs);\n\n    lhs.mSelector = 0;\n    EXPECT_NE(lhs, rhs);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, cf_string_to_std_string)\n{\n    auto std_string = apple_coreaudio::cf_string_to_std_string(CFSTR(\"TestString\"));\n    EXPECT_EQ(std_string, \"TestString\");\n}\n\n/**\n * Little helper class which publicly exposes protected methods.\n */\nclass MockAudioObject : public apple_coreaudio::AudioObject\n{\npublic:\n    explicit MockAudioObject(AudioObjectID audio_object_id) : AudioObject(audio_object_id) {}\n\n    MOCK_METHOD(void, property_changed, (const AudioObjectPropertyAddress& address));\n};\n\nTEST_F(TestAppleCoreAudioFrontend, AudioObject_get_audio_object_id)\n{\n    MockAudioObject audio_object0(0);\n    EXPECT_EQ(audio_object0.get_audio_object_id(), 0);\n\n    MockAudioObject audio_object1(1);\n    EXPECT_EQ(audio_object1.get_audio_object_id(), 1);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioObject_is_valid)\n{\n    // Zero is not considered a valid object ID.\n    MockAudioObject audio_object0(0);\n    EXPECT_FALSE(audio_object0.is_valid());\n\n    // Anything higher than zero is considered a valid object ID.\n    MockAudioObject audio_object1(1);\n    EXPECT_TRUE(audio_object1.is_valid());\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioObject_has_property)\n{\n    MockAudioObject audio_object(0);\n\n    EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(false));\n    EXPECT_FALSE(audio_object.has_property({0, 0, 0}));\n\n    EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n    EXPECT_TRUE(audio_object.has_property({0, 0, 0}));\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioObject_is_property_settable)\n{\n    MockAudioObject audio_object(0);\n\n    EXPECT_CALL(_mock, AudioObjectIsPropertySettable).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, Boolean* out_is_settable) {\n        *out_is_settable = false;\n        return kAudioHardwareNoError;\n    });\n    EXPECT_FALSE(audio_object.is_property_settable({0, 0, 0}));\n\n    EXPECT_CALL(_mock, AudioObjectIsPropertySettable).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, Boolean* out_is_settable) {\n        *out_is_settable = true;\n        return kAudioHardwareNoError;\n    });\n    EXPECT_TRUE(audio_object.is_property_settable({0, 0, 0}));\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioObject_get_property_data_size)\n{\n    MockAudioObject audio_object(0);\n\n    EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n        *out_data_size = sizeof(UInt32);\n        return kAudioHardwareNoError;\n    });\n    EXPECT_EQ(audio_object.get_property_data_size({1, 1, 1}), sizeof(UInt32));\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioObject_get_property_data)\n{\n    MockAudioObject audio_object(0);\n\n    EXPECT_CALL(_mock, AudioObjectGetPropertyData).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* data_size, void* out_data) {\n        *data_size = sizeof(UInt32);\n        *static_cast<UInt32*>(out_data) = 5;\n        return kAudioHardwareNoError;\n    });\n    UInt32 data;\n    EXPECT_EQ(audio_object.get_property_data({1, 1, 1}, sizeof(UInt32), &data), sizeof(UInt32));\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioObject_set_property_data)\n{\n    MockAudioObject audio_object(0);\n\n    EXPECT_CALL(_mock, AudioObjectSetPropertyData).WillOnce(Return(kAudioHardwareNoError));\n\n    UInt32 data;\n    EXPECT_TRUE(audio_object.set_property_data({1, 1, 1}, sizeof(UInt32), &data));\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioObject_get_property)\n{\n    MockAudioObject audio_object(2);\n\n    EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n    EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n        *out_data_size = sizeof(UInt32);\n        return kAudioHardwareNoError;\n    });\n    EXPECT_CALL(_mock, AudioObjectGetPropertyData).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* data_size, void* out_data) {\n        *data_size = sizeof(UInt32);\n        *static_cast<UInt32*>(out_data) = 5;\n        return kAudioHardwareNoError;\n    });\n    EXPECT_EQ(audio_object.get_property<UInt32>({1, 1, 1}), 5);\n\n    { // If the property has a data size which is not equal to the size of the data type\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n        EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n            *out_data_size = sizeof(UInt32) + 1;\n            return kAudioHardwareNoError;\n        });\n        EXPECT_FALSE(audio_object.get_property<UInt32>(2, {1, 1, 1}));\n    }\n\n    { // If the property get data returns an invalid data size\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n        EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n            *out_data_size = sizeof(UInt32);\n            return kAudioHardwareNoError;\n        });\n        EXPECT_CALL(_mock, AudioObjectGetPropertyData).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* data_size, void* out_data) {\n            *data_size = sizeof(UInt32) + 1;\n            *static_cast<UInt32*>(out_data) = 5;\n            return kAudioHardwareNoError;\n        });\n        EXPECT_FALSE(audio_object.get_property<UInt32>(2, {1, 1, 1}));\n    }\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioObject_set_property)\n{\n    MockAudioObject audio_object(2);\n\n    EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n    EXPECT_CALL(_mock, AudioObjectIsPropertySettable).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, Boolean* out_is_settable) {\n        *out_is_settable = true;\n        return kAudioHardwareNoError;\n    });\n    EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n        *out_data_size = sizeof(UInt32);\n        return kAudioHardwareNoError;\n    });\n    EXPECT_CALL(_mock, AudioObjectSetPropertyData).WillOnce(Return(kAudioHardwareNoError));\n    EXPECT_TRUE(audio_object.set_property<UInt32>({1, 1, 1}, 5));\n\n    { // If the object has no property, expect false\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(false));\n        EXPECT_FALSE(audio_object.set_property<UInt32>(2, {1, 1, 1}, 5));\n    }\n\n    { // If property is not settable, expect false\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n        EXPECT_CALL(_mock, AudioObjectIsPropertySettable).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, Boolean* out_is_settable) {\n            *out_is_settable = false;\n            return kAudioHardwareNoError;\n        });\n        EXPECT_FALSE(audio_object.set_property<UInt32>(2, {1, 1, 1}, 5));\n    }\n\n    { // If the data size does not match the size of the type, expect false\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n        EXPECT_CALL(_mock, AudioObjectIsPropertySettable).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, Boolean* out_is_settable) {\n            *out_is_settable = true;\n            return kAudioHardwareNoError;\n        });\n        EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n            *out_data_size = sizeof(UInt32) + 1;\n            return kAudioHardwareNoError;\n        });\n        EXPECT_FALSE(audio_object.set_property<UInt32>(2, {1, 1, 1}, 5));\n    }\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioObject_get_cf_string_property)\n{\n    MockAudioObject audio_object(2);\n\n    CFStringRef string_ref = CFSTR(\"SomeTestString\");\n\n    EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n    EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n        *out_data_size = sizeof(CFStringRef);\n        return kAudioHardwareNoError;\n    });\n    EXPECT_CALL(_mock, AudioObjectGetPropertyData).WillOnce([&string_ref](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* data_size, void* out_data) {\n        *data_size = sizeof(CFStringRef);\n        *static_cast<CFStringRef*>(out_data) = string_ref;\n        return kAudioHardwareNoError;\n    });\n    EXPECT_EQ(audio_object.get_cfstring_property({1, 1, 1}), \"SomeTestString\");\n\n    // When the object doesn't have a CFString property at given address, it should return an empty string.\n    EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(false));\n    EXPECT_EQ(audio_object.get_cfstring_property({1, 1, 1}), \"\");\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioObject_get_property_array)\n{\n    MockAudioObject audio_object(2);\n\n    EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n    EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n        *out_data_size = 3 * sizeof(UInt32);\n        return kAudioHardwareNoError;\n    });\n    EXPECT_CALL(_mock, AudioObjectGetPropertyData).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* data_size, void* out_data) {\n        *data_size = 3 * sizeof(UInt32);\n        static_cast<UInt32*>(out_data)[0] = 1;\n        static_cast<UInt32*>(out_data)[1] = 2;\n        static_cast<UInt32*>(out_data)[2] = 3;\n        return kAudioHardwareNoError;\n    });\n\n    EXPECT_EQ(audio_object.get_property_array<UInt32>({1, 1, 1}), std::vector<UInt32>({1, 2, 3}));\n\n    { // If the object has no property at given address, expect an empty vector.\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(false));\n\n        std::vector<UInt32> vector;\n        EXPECT_FALSE(audio_object.get_property_array<UInt32>(2, {1, 1, 1}, vector));\n        EXPECT_TRUE(vector.empty());\n    }\n\n    { // If the property reports a data size which is not a multiple of sizeof(Type), then expect an empty array\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n        EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n            *out_data_size = 3 * sizeof(UInt32) + 1;\n            return kAudioHardwareNoError;\n        });\n        std::vector<UInt32> vector;\n        EXPECT_FALSE(audio_object.get_property_array<UInt32>(2, {1, 1, 1}, vector));\n        EXPECT_TRUE(vector.empty());\n    }\n\n    { // If the property reports a data size of zero, then expect an empty array\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n        EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n            *out_data_size = 0;\n            return kAudioHardwareNoError;\n        });\n        std::vector<UInt32> vector;\n        EXPECT_TRUE(audio_object.get_property_array<UInt32>(2, {1, 1, 1}, vector));\n        EXPECT_TRUE(vector.empty());\n    }\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioObject_add_property_listener)\n{\n    AudioObjectID audio_object_id(2);\n    testing::StrictMock<MockAudioObject> audio_object(audio_object_id);\n\n    AudioObjectPropertyListenerProc listener_proc;\n    void* client_data = nullptr;\n\n    EXPECT_CALL(_mock, AudioObjectAddPropertyListener).WillOnce([&listener_proc, &client_data](AudioObjectID, const AudioObjectPropertyAddress*, AudioObjectPropertyListenerProc inListener, void* inClientData) {\n        listener_proc = inListener;\n        client_data = inClientData;\n        return kAudioHardwareNoError;\n    });\n\n    EXPECT_TRUE(audio_object.add_property_listener({1, 1, 1}));\n\n    EXPECT_CALL(audio_object, property_changed);\n\n    AudioObjectPropertyAddress pa{1, 1, 1};\n    EXPECT_EQ(listener_proc(audio_object_id, 1, &pa, client_data), kAudioHardwareNoError);\n\n    // When no client data is provided the listener function should return an error\n    EXPECT_EQ(listener_proc(audio_object_id, 1, &pa, nullptr), kAudioHardwareBadObjectError);\n\n    // When the wrong object id is given, the listener function should return an error\n    EXPECT_EQ(listener_proc(audio_object_id + 1, 1, &pa, client_data), kAudioHardwareBadObjectError);\n\n    EXPECT_CALL(_mock, AudioObjectAddPropertyListener);\n\n    // Adding the same property will not add ake AudioObject to add another listener, but will return true.\n    EXPECT_TRUE(audio_object.add_property_listener({1, 1, 1}));\n\n    EXPECT_TRUE(audio_object.add_property_listener({2, 2, 2}));\n\n    // When the AudioObject goes out of scope, property listeners should be removed\n    EXPECT_CALL(_mock, AudioObjectRemovePropertyListener).Times(2);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioSystemObject_get_audio_devices)\n{\n    EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n    EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* out_data_size) {\n        EXPECT_EQ(audio_object_id, kAudioObjectSystemObject);\n        AudioObjectPropertyAddress expected{kAudioHardwarePropertyDevices,\n                                            kAudioObjectPropertyScopeGlobal,\n                                            kAudioObjectPropertyElementMain};\n        EXPECT_EQ(*address, expected);\n        *out_data_size = 3 * sizeof(UInt32);\n        return kAudioHardwareNoError;\n    });\n    EXPECT_CALL(_mock, AudioObjectGetPropertyData).WillOnce([](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* data_size, void* out_data) {\n        EXPECT_EQ(audio_object_id, kAudioObjectSystemObject);\n        AudioObjectPropertyAddress expected{kAudioHardwarePropertyDevices,\n                                            kAudioObjectPropertyScopeGlobal,\n                                            kAudioObjectPropertyElementMain};\n        EXPECT_EQ(*address, expected);\n        *data_size = 3 * sizeof(UInt32);\n        static_cast<UInt32*>(out_data)[0] = 2;\n        static_cast<UInt32*>(out_data)[1] = 3;\n        static_cast<UInt32*>(out_data)[2] = 4;\n        return kAudioHardwareNoError;\n    });\n\n    auto devices = apple_coreaudio::AudioSystemObject::get_audio_devices();\n\n    EXPECT_EQ(devices.size(), 3);\n    EXPECT_EQ(devices[0].get_audio_object_id(), 2);\n    EXPECT_EQ(devices[1].get_audio_object_id(), 3);\n    EXPECT_EQ(devices[2].get_audio_object_id(), 4);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioSystemObject_get_default_device_id)\n{\n    { // For input\n        AudioObjectPropertyAddress expected_addr{kAudioHardwarePropertyDefaultInputDevice,\n                                                 kAudioObjectPropertyScopeGlobal,\n                                                 kAudioObjectPropertyElementMain};\n\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n        EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([&expected_addr](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* out_data_size) {\n            EXPECT_EQ(audio_object_id, kAudioObjectSystemObject);\n            EXPECT_EQ(*address, expected_addr);\n            *out_data_size = sizeof(UInt32);\n            return kAudioHardwareNoError;\n        });\n        EXPECT_CALL(_mock, AudioObjectGetPropertyData).WillOnce([&expected_addr](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* data_size, void* out_data) {\n            EXPECT_EQ(audio_object_id, kAudioObjectSystemObject);\n            EXPECT_EQ(*address, expected_addr);\n            *data_size = sizeof(UInt32);\n            *static_cast<UInt32*>(out_data) = 5;\n            return kAudioHardwareNoError;\n        });\n        EXPECT_EQ(apple_coreaudio::AudioSystemObject::get_default_device_id(true), 5);\n    }\n\n    { // For output\n        AudioObjectPropertyAddress expected_addr{kAudioHardwarePropertyDefaultOutputDevice,\n                                                 kAudioObjectPropertyScopeGlobal,\n                                                 kAudioObjectPropertyElementMain};\n\n        EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n        EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillOnce([&expected_addr](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* out_data_size) {\n            EXPECT_EQ(audio_object_id, kAudioObjectSystemObject);\n            EXPECT_EQ(*address, expected_addr);\n            *out_data_size = sizeof(UInt32);\n            return kAudioHardwareNoError;\n        });\n        EXPECT_CALL(_mock, AudioObjectGetPropertyData).WillOnce([&expected_addr](AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32, const void*, UInt32* data_size, void* out_data) {\n            EXPECT_EQ(audio_object_id, kAudioObjectSystemObject);\n            EXPECT_EQ(*address, expected_addr);\n            *data_size = sizeof(UInt32);\n            *static_cast<UInt32*>(out_data) = 5;\n            return kAudioHardwareNoError;\n        });\n        EXPECT_EQ(apple_coreaudio::AudioSystemObject::get_default_device_id(false), 5);\n    }\n}\n\nstatic OSStatus dummy_audio_device_io_proc(AudioObjectID,\n                                           const AudioTimeStamp*,\n                                           const AudioBufferList*,\n                                           const AudioTimeStamp*,\n                                           AudioBufferList*,\n                                           const AudioTimeStamp*,\n                                           void*) { return kAudioHardwareNoError; }\n\nauto assign_dummy_io_proc = [](AudioObjectID, AudioDeviceIOProc, void*, AudioDeviceIOProcID* proc_id) {\n    *proc_id = dummy_audio_device_io_proc; // AudioDevice needs this in order to make stop_io() do something.\n    return kAudioHardwareNoError;\n};\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_start_io)\n{\n    apple_coreaudio::AudioDevice::AudioCallback callback;\n\n    {\n        apple_coreaudio::AudioDevice invalid_audio_device(0);\n        EXPECT_FALSE(invalid_audio_device.start_io(&callback))\n                << \"Refuse to start an audio device when the AudioObjectID is invalid.\";\n    }\n\n    apple_coreaudio::AudioDevice audio_device(5);\n\n    EXPECT_FALSE(audio_device.start_io(nullptr))\n            << \"When the callback is nullptr, the audio device should not start.\";\n\n    EXPECT_CALL(_mock, AudioDeviceCreateIOProcID).WillOnce(assign_dummy_io_proc);\n    EXPECT_CALL(_mock, AudioDeviceStart);\n    EXPECT_CALL(_mock, AudioObjectAddPropertyListener);\n\n    EXPECT_TRUE(audio_device.start_io(&callback));\n\n    // At destruction the audio device should properly close and stop etc.\n    EXPECT_CALL(_mock, AudioObjectRemovePropertyListener);\n    EXPECT_CALL(_mock, AudioDeviceStop);\n    EXPECT_CALL(_mock, AudioDeviceDestroyIOProcID);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_stop_io)\n{\n    apple_coreaudio::AudioDevice::AudioCallback callback;\n    apple_coreaudio::AudioDevice audio_device(5);\n\n    EXPECT_CALL(_mock, AudioDeviceCreateIOProcID).WillOnce(assign_dummy_io_proc);\n    EXPECT_CALL(_mock, AudioDeviceStart);\n    EXPECT_CALL(_mock, AudioObjectAddPropertyListener);\n    audio_device.start_io(&callback);\n\n    EXPECT_CALL(_mock, AudioDeviceStop);\n    EXPECT_CALL(_mock, AudioDeviceDestroyIOProcID);\n    audio_device.stop_io();\n\n    // Expected at device destruction\n    EXPECT_CALL(_mock, AudioObjectRemovePropertyListener);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_get_name)\n{\n    apple_coreaudio::AudioDevice audio_device(5);\n\n    auto device_name = CFSTR(\"device_name\");\n\n    expect_calls_to_get_cf_string_property(5, {kAudioObjectPropertyName, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain},\n                                           device_name);\n    EXPECT_EQ(audio_device.name(), \"device_name\");\n\n    apple_coreaudio::AudioDevice invalid_audio_device(0);\n    EXPECT_EQ(invalid_audio_device.name(), \"\");\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_get_uid)\n{\n    apple_coreaudio::AudioDevice audio_device(5);\n\n    auto device_uid = CFSTR(\"device_uid\");\n\n    expect_calls_to_get_cf_string_property(5, {kAudioDevicePropertyDeviceUID, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain},\n                                           device_uid);\n    EXPECT_EQ(audio_device.uid(), \"device_uid\");\n\n    apple_coreaudio::AudioDevice invalid_audio_device(0);\n    EXPECT_EQ(invalid_audio_device.uid(), \"\");\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_get_num_channels)\n{\n    apple_coreaudio::AudioDevice invalid_audio_device(0);\n    EXPECT_EQ(invalid_audio_device.num_channels(true), -1);\n\n    // Return -1 when the object has no stream configuration property.\n    apple_coreaudio::AudioDevice audio_device(5);\n    EXPECT_CALL(_mock, AudioObjectHasProperty)\n            .WillOnce(Return(false));\n\n    EXPECT_EQ(audio_device.num_channels(true), -1);\n\n    EXPECT_CALL(_mock, AudioObjectHasProperty)\n            .WillOnce(Return(true))\n            .WillOnce(Return(true));\n\n    EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize)\n            .WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n                *out_data_size = sizeof(AudioBufferList) + 2 * sizeof(AudioBuffer);\n                return kAudioHardwareNoError;\n            })\n            .WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n                *out_data_size = sizeof(AudioBufferList);\n                return kAudioHardwareNoError;\n            });\n    \n    EXPECT_CALL(_mock, AudioObjectGetPropertyData)\n            .WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* data_size, void* out_data) {\n                *data_size = sizeof(AudioBufferList) + 2 * sizeof(AudioBuffer);\n                auto* audio_buffer_lists = static_cast<AudioBufferList*>(out_data);\n                audio_buffer_lists[0].mNumberBuffers = 3;\n                audio_buffer_lists[0].mBuffers[0].mNumberChannels = 1;\n                audio_buffer_lists[0].mBuffers[1].mNumberChannels = 2;\n                audio_buffer_lists[0].mBuffers[2].mNumberChannels = 3;\n                return kAudioHardwareNoError;\n            })\n            .WillOnce([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* data_size, void* out_data) {\n                *data_size = sizeof(AudioBufferList);\n                auto* audio_buffer_lists = static_cast<AudioBufferList*>(out_data);\n                audio_buffer_lists[0].mNumberBuffers = 0;\n                return kAudioHardwareNoError;\n            });\n\n    // By default, an audio device selects the first stream only, so while there are multiple streams (buffers) available we expect a channel count of 1.\n    EXPECT_EQ(audio_device.num_channels(true), 1);\n    EXPECT_EQ(audio_device.num_channels(true), 0);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_set_buffer_frame_size)\n{\n    apple_coreaudio::AudioDevice invalid_audio_device(0);\n    EXPECT_FALSE(invalid_audio_device.set_buffer_frame_size(512));\n\n    apple_coreaudio::AudioDevice audio_device(5);\n\n    expect_calls_to_set_property<UInt32>(5, {kAudioDevicePropertyBufferFrameSize,\n                                             kAudioObjectPropertyScopeGlobal,\n                                             kAudioObjectPropertyElementMain});\n\n    EXPECT_TRUE(audio_device.set_buffer_frame_size(512));\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_set_nominal_sample_rate)\n{\n    apple_coreaudio::AudioDevice invalid_audio_device(0);\n    EXPECT_FALSE(invalid_audio_device.set_nominal_sample_rate(48000.0));\n\n    apple_coreaudio::AudioDevice audio_device(5);\n\n    expect_calls_to_set_property<double>(5, {kAudioDevicePropertyNominalSampleRate,\n                                             kAudioObjectPropertyScopeGlobal,\n                                             kAudioObjectPropertyElementMain});\n\n    EXPECT_TRUE(audio_device.set_nominal_sample_rate(48000.0));\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_get_nominal_sample_rate)\n{\n    apple_coreaudio::AudioDevice invalid_audio_device(0);\n    EXPECT_EQ(invalid_audio_device.nominal_sample_rate(), 0.0);\n\n    apple_coreaudio::AudioDevice audio_device(5);\n\n    expect_calls_to_get_property<double>(5, {kAudioDevicePropertyNominalSampleRate, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain}, 48000.0);\n\n    EXPECT_EQ(audio_device.nominal_sample_rate(), 48000.0);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_get_device_latency)\n{\n    apple_coreaudio::AudioDevice invalid_audio_device(0);\n    EXPECT_EQ(invalid_audio_device.device_latency(true), 0);\n\n    apple_coreaudio::AudioDevice audio_device(5);\n\n    expect_calls_to_get_property<UInt32>(5, {kAudioDevicePropertyLatency, kAudioObjectPropertyScopeInput, kAudioObjectPropertyElementMain}, 320);\n\n    EXPECT_EQ(audio_device.device_latency(true), 320);\n\n    expect_calls_to_get_property<UInt32>(5, {kAudioDevicePropertyLatency, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain}, 330);\n\n    EXPECT_EQ(audio_device.device_latency(false), 330);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_get_stream_latency)\n{\n    apple_coreaudio::AudioDevice invalid_audio_device(0);\n    EXPECT_EQ(invalid_audio_device.stream_latency(0, true), 0);\n\n    EXPECT_CALL(_mock, AudioObjectHasProperty).WillRepeatedly(Return(true));\n    EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillRepeatedly([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n        *out_data_size = sizeof(UInt32);\n        return kAudioHardwareNoError;\n    });\n    EXPECT_CALL(_mock, AudioObjectGetPropertyData)\n            .WillOnce([](AudioObjectID, const AudioObjectPropertyAddress* pa, UInt32, const void*, UInt32* data_size, void* out_data) {\n                EXPECT_EQ(*pa, AudioObjectPropertyAddress({kAudioDevicePropertyStreams,\n                                                           kAudioObjectPropertyScopeInput,\n                                                           kAudioObjectPropertyElementMain}));\n                *data_size = sizeof(UInt32);\n                static_cast<UInt32*>(out_data)[0] = 1; // This is the id of stream with index 0\n                return kAudioHardwareNoError;\n            })\n            .WillOnce([](AudioObjectID, const AudioObjectPropertyAddress* pa, UInt32, const void*, UInt32* data_size, void* out_data) {\n                EXPECT_EQ(*pa, AudioObjectPropertyAddress({kAudioStreamPropertyLatency,\n                                                           kAudioObjectPropertyScopeInput,\n                                                           kAudioObjectPropertyElementMain}));\n                *data_size = sizeof(UInt32);\n                static_cast<UInt32*>(out_data)[0] = 16;\n                return kAudioHardwareNoError;\n            })\n            .WillOnce([](AudioObjectID, const AudioObjectPropertyAddress* pa, UInt32, const void*, UInt32* data_size, void*) {\n                EXPECT_EQ(*pa, AudioObjectPropertyAddress({kAudioDevicePropertyStreams,\n                                                           kAudioObjectPropertyScopeInput,\n                                                           kAudioObjectPropertyElementMain}));\n                *data_size = 0;\n                return kAudioHardwareBadObjectError;\n            });\n\n    apple_coreaudio::AudioDevice audio_device(1);\n\n    EXPECT_EQ(audio_device.stream_latency(0, true), 16);\n\n    // Test out of bounds stream\n    EXPECT_EQ(audio_device.stream_latency(1, true), 0);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_get_selected_stream_latency)\n{\n    EXPECT_CALL(_mock, AudioObjectHasProperty).WillRepeatedly(Return(true));\n\n    EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillRepeatedly([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n        *out_data_size = sizeof(UInt32);\n        return kAudioHardwareNoError;\n    });\n\n    EXPECT_CALL(_mock, AudioObjectGetPropertyData)\n            .WillOnce(Return(kAudioHardwareNoError))\n            .WillOnce([](AudioObjectID, const AudioObjectPropertyAddress* pa, UInt32, const void*, UInt32* data_size, void* out_data) {\n                EXPECT_EQ(*pa, AudioObjectPropertyAddress({kAudioStreamPropertyLatency,\n                                                           kAudioObjectPropertyScopeInput,\n                                                           kAudioObjectPropertyElementMain}));\n                *data_size = sizeof(UInt32);\n                static_cast<UInt32*>(out_data)[0] = 16;\n                return kAudioHardwareNoError;\n            })\n            .WillOnce(Return(kAudioHardwareNoError))\n            .WillOnce([](AudioObjectID, const AudioObjectPropertyAddress* pa, UInt32, const void*, UInt32* data_size, void* out_data) {\n                EXPECT_EQ(*pa, AudioObjectPropertyAddress({kAudioStreamPropertyLatency,\n                                                           kAudioObjectPropertyScopeOutput,\n                                                           kAudioObjectPropertyElementMain}));\n                *data_size = sizeof(UInt32);\n                static_cast<UInt32*>(out_data)[0] = 16;\n                return kAudioHardwareNoError;\n            });\n\n    apple_coreaudio::AudioDevice audio_device(1);\n    EXPECT_EQ(audio_device.selected_stream_latency(true), 16);\n    EXPECT_EQ(audio_device.selected_stream_latency(false), 16);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_get_clock_domain_id)\n{\n    apple_coreaudio::AudioDevice invalid_audio_device(0);\n    EXPECT_EQ(invalid_audio_device.clock_domain_id(), 0);\n\n    apple_coreaudio::AudioDevice audio_device(1);\n    expect_calls_to_get_property(1, {kAudioDevicePropertyClockDomain, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain}, 5);\n    EXPECT_EQ(audio_device.clock_domain_id(), 5);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_get_related_devices)\n{\n    EXPECT_CALL(_mock, AudioObjectHasProperty).WillOnce(Return(true));\n\n    EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize).WillRepeatedly([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n        *out_data_size = 3 * sizeof(UInt32);\n        return kAudioHardwareNoError;\n    });\n\n    EXPECT_CALL(_mock, AudioObjectGetPropertyData)\n            .WillOnce([](AudioObjectID, const AudioObjectPropertyAddress* pa, UInt32, const void*, UInt32* data_size, void* out_data) {\n                EXPECT_EQ(*pa, AudioObjectPropertyAddress({kAudioDevicePropertyRelatedDevices,\n                                                           kAudioObjectPropertyScopeGlobal,\n                                                           kAudioObjectPropertyElementMain}));\n                *data_size = 3 * sizeof(UInt32);\n                static_cast<UInt32*>(out_data)[0] = 1;\n                static_cast<UInt32*>(out_data)[1] = 2;\n                static_cast<UInt32*>(out_data)[2] = 3;\n                return kAudioHardwareNoError;\n            });\n\n    apple_coreaudio::AudioDevice audio_device(1);\n    auto devices = audio_device.related_devices();\n    EXPECT_EQ(devices.at(0), 1);\n    EXPECT_EQ(devices.at(1), 2);\n    EXPECT_EQ(devices.at(2), 3);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_get_num_streams)\n{\n    EXPECT_CALL(_mock, AudioObjectHasProperty)\n            .WillRepeatedly(Return(true));\n\n    EXPECT_CALL(_mock, AudioObjectGetPropertyDataSize)\n            .WillRepeatedly([](AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* out_data_size) {\n                *out_data_size = 3 * sizeof(UInt32);\n                return kAudioHardwareNoError;\n            });\n\n    EXPECT_CALL(_mock, AudioObjectGetPropertyData)\n            .WillOnce([](AudioObjectID, const AudioObjectPropertyAddress* pa, UInt32, const void*, UInt32* data_size, void* out_data) {\n                EXPECT_EQ(*pa, AudioObjectPropertyAddress({kAudioDevicePropertyStreams,\n                                                           kAudioObjectPropertyScopeInput,\n                                                           kAudioObjectPropertyElementMain}));\n                *data_size = 3 * sizeof(UInt32);\n                static_cast<UInt32*>(out_data)[0] = 1;\n                static_cast<UInt32*>(out_data)[1] = 2;\n                static_cast<UInt32*>(out_data)[2] = 3;\n                return kAudioHardwareNoError;\n            })\n            .WillOnce([](AudioObjectID, const AudioObjectPropertyAddress* pa, UInt32, const void*, UInt32* data_size, void* out_data) {\n                EXPECT_EQ(*pa, AudioObjectPropertyAddress({kAudioDevicePropertyStreams,\n                                                           kAudioObjectPropertyScopeOutput,\n                                                           kAudioObjectPropertyElementMain}));\n                *data_size = 3 * sizeof(UInt32);\n                static_cast<UInt32*>(out_data)[0] = 4;\n                static_cast<UInt32*>(out_data)[1] = 5;\n                static_cast<UInt32*>(out_data)[2] = 6;\n                return kAudioHardwareNoError;\n            });\n\n    apple_coreaudio::AudioDevice audio_device(1);\n    EXPECT_EQ(audio_device.num_streams(true), 3);\n    EXPECT_EQ(audio_device.num_streams(false), 3);\n}\n\nTEST_F(TestAppleCoreAudioFrontend, AudioDevice_is_aggregate_device)\n{\n    expect_calls_to_get_property(5, {kAudioObjectPropertyClass, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain}, kAudioAggregateDeviceClassID);\n    apple_coreaudio::AudioDevice audio_device(5);\n    EXPECT_TRUE(audio_device.is_aggregate_device());\n}"
  },
  {
    "path": "test/unittests/audio_frontends/jack_frontend_test.cpp",
    "content": "#include <functional>\n\n#include \"gtest/gtest.h\"\n\n#include \"test_utils/engine_mockup.h\"\n#include \"test_utils/jack_mockup.cpp\"\n#ifdef SUSHI_BUILD_WITH_ALSA_MIDI\n#include \"control_frontends/alsa_midi_frontend.cpp\"\n#endif\n#include \"engine/midi_dispatcher.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"audio_frontends/jack_frontend.cpp\"\n\nusing ::testing::internal::posix::GetEnv;\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::audio_frontend;\nusing namespace sushi::internal::midi_dispatcher;\n\nconstexpr float SAMPLE_RATE = 44000;\nconstexpr int CV_CHANNELS = 0;\n\nnamespace sushi::internal::audio_frontend {\nclass JackFrontendAccessor\n{\npublic:\n    explicit JackFrontendAccessor(JackFrontend* f) : _friend(f) {}\n\n    jack_client_t* client()\n    {\n        return _friend->_client;\n    }\n\nprivate:\n    JackFrontend* _friend;\n};\n}\n\nclass TestJackFrontend : public ::testing::Test\n{\nprotected:\n    TestJackFrontend() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<JackFrontend>(&_engine);\n        _accessor = std::make_unique<JackFrontendAccessor>(_module_under_test.get());\n    }\n\n    void TearDown() override\n    {\n        _module_under_test->cleanup();\n    }\n\n    EngineMockup _engine {SAMPLE_RATE};\n\n    std::unique_ptr<JackFrontend> _module_under_test;\n    std::unique_ptr<JackFrontendAccessor> _accessor;\n};\n\nTEST_F(TestJackFrontend, TestOperation)\n{\n    JackFrontendConfiguration config(\"Jack Client\", \"Jack Server\", false, CV_CHANNELS, CV_CHANNELS);\n    auto ret_code = _module_under_test->init(&config);\n    ASSERT_EQ(AudioFrontendStatus::OK, ret_code);\n\n    /* Can't call run directly because that will freeze the test due to the sleep() call*/\n    jack_activate(_accessor->client());\n\n    ASSERT_TRUE(_engine.process_called);\n}\n\n\n"
  },
  {
    "path": "test/unittests/audio_frontends/offline_frontend_test.cpp",
    "content": "#include <fstream>\n#include \"gtest/gtest.h\"\n\n#include \"test_utils/engine_mockup.h\"\n#include \"engine/json_configurator.h\"\n#include \"test_utils/test_utils.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"audio_frontends/offline_frontend.cpp\"\n\n#include \"utils.cpp\"\n\nusing ::testing::internal::posix::GetEnv;\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::audio_frontend;\nusing namespace sushi::internal::midi_dispatcher;\n\nconstexpr float SAMPLE_RATE = 44000;\nconstexpr int CV_CHANNELS = 0;\nconstexpr int AUDIO_CHANNELS = 2;\n\nnamespace sushi::internal::audio_frontend\n{\n\nclass OfflineFrontendAccessor\n{\npublic:\n    explicit OfflineFrontendAccessor(OfflineFrontend& f) : _friend(f) {}\n\n    const std::vector<std::unique_ptr<Event>>& event_queue()\n    {\n        return _friend._event_queue;\n    }\n\nprivate:\n    OfflineFrontend& _friend;\n};\n\n}\n\nclass TestOfflineFrontend : public ::testing::Test\n{\nprotected:\n    TestOfflineFrontend() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<OfflineFrontend>(&_engine);\n\n        _accessor = std::make_unique<OfflineFrontendAccessor>(*_module_under_test);\n\n        _engine.set_audio_channels(AUDIO_CHANNELS, AUDIO_CHANNELS);\n    }\n\n    EngineMockup _engine {SAMPLE_RATE};\n    MidiDispatcher _midi_dispatcher {_engine.event_dispatcher()};\n    std::unique_ptr<OfflineFrontend> _module_under_test ;\n    std::unique_ptr<OfflineFrontendAccessor> _accessor;\n};\n\nTEST_F(TestOfflineFrontend, TestWavProcessing)\n{\n    char const* test_data_dir = GetEnv(\"SUSHI_TEST_DATA_DIR\");\n    if (test_data_dir == nullptr)\n    {\n        EXPECT_TRUE(false) << \"Can't access Test Data environment variable\";\n    }\n\n    // Initialize with a file containing 0.5 on both channels\n    std::string test_data_file(test_data_dir);\n    test_data_file.append(\"/test_sndfile_05.wav\");\n    std::string output_file_name(\"./test_out.wav\");\n    OfflineFrontendConfiguration config(test_data_file, \"./test_out.wav\", false, CV_CHANNELS, CV_CHANNELS);\n    auto ret_code = _module_under_test->init(&config);\n    if (ret_code != AudioFrontendStatus::OK)\n    {\n        EXPECT_TRUE(false) << \"Error initializing Frontend\";\n    }\n    // Process with the dummy bypass engine\n    _module_under_test->run();\n\n    // Read the generated file and verify the result\n    SNDFILE* output_file;\n    SF_INFO soundfile_info;\n\n    memset(&soundfile_info, 0, sizeof(soundfile_info));\n\n    if (! (output_file = sf_open(output_file_name.c_str(), SFM_READ, &soundfile_info)) )\n    {\n        EXPECT_TRUE(false) << \"Error opening output file: \" << output_file_name;\n    }\n\n    float file_buffer[AUDIO_CHANNELS * AUDIO_CHUNK_SIZE];\n    unsigned int readcount;\n    while ( (readcount = static_cast<unsigned int>(sf_readf_float(output_file,\n                                                         &file_buffer[0],\n                                                         static_cast<sf_count_t>(AUDIO_CHUNK_SIZE)))) )\n    {\n        for (unsigned int n = 0; n < (readcount * AUDIO_CHANNELS); n++)\n        {\n            ASSERT_FLOAT_EQ(0.5f, file_buffer[n]);\n        }\n    }\n\n    sf_close(output_file);\n}\n\nTEST_F(TestOfflineFrontend, TestInvalidInputFile)\n{\n    OfflineFrontendConfiguration config(\"this_is_not_a_valid_file.extension\", \"./test_out.wav\", false, CV_CHANNELS, CV_CHANNELS);\n    auto ret_code = _module_under_test->init(&config);\n    ASSERT_EQ(AudioFrontendStatus::INVALID_INPUT_FILE, ret_code);\n}\n\nTEST_F(TestOfflineFrontend, TestMonoMode)\n{\n    char const* test_data_dir = GetEnv(\"SUSHI_TEST_DATA_DIR\");\n    if (test_data_dir == nullptr)\n    {\n        EXPECT_TRUE(false) << \"Can't access Test Data environment variable\";\n    }\n    // Initialize with a mono file\n    std::string test_data_file(test_data_dir);\n    test_data_file.append(\"/mono.wav\");\n    std::string output_file_name(\"./test_out.wav\");\n    OfflineFrontendConfiguration config(test_data_file, \"./test_out.wav\", false, CV_CHANNELS, CV_CHANNELS);\n    auto ret_code = _module_under_test->init(&config);\n    ASSERT_EQ(AudioFrontendStatus::OK, ret_code);\n\n    // Process with the dummy bypass engine and make check this doesn't crash\n    _module_under_test->run();\n}\n\nTEST_F(TestOfflineFrontend, TestAddSequencerEvents)\n{\n    char const* test_data_dir = GetEnv(\"SUSHI_TEST_DATA_DIR\");\n    if (test_data_dir == nullptr)\n    {\n        EXPECT_TRUE(false) << \"Can't access Test Data environment variable\\n\";\n    }\n\n    // Initialize with a file containing 0.5 on both channels\n    std::string test_config_file = test_utils::get_data_dir_path();\n    test_config_file.append(\"config.json\");\n\n    auto json_data = read_file(test_config_file);\n\n    jsonconfig::JsonConfigurator configurator(&_engine,\n                                              &_midi_dispatcher,\n                                              _engine.processor_container(),\n                                              json_data.value());\n    rapidjson::Document config;\n    auto [status, events] = configurator.load_event_list();\n    ASSERT_EQ(jsonconfig::JsonConfigReturnStatus::OK, status);\n    _module_under_test->add_sequencer_events(std::move(events));\n\n    auto event_q = &_accessor->event_queue();\n    ASSERT_EQ(4u, event_q->size());\n\n    // Check that queue is sorted by time\n    auto jt = --event_q->end();\n    for(auto it = event_q->begin(); it != jt; ++it)\n    {\n        ASSERT_GE((*it)->time(), (*(it + 1))->time());\n    }\n}\n\nTEST_F(TestOfflineFrontend, TestNoiseGeneration)\n{\n    ChunkSampleBuffer buffer(2);\n    std::ranlux24 rand_gen;\n    rand_gen.seed(NOISE_SEED);\n    std::normal_distribution<float> normal_dist(0, INPUT_NOISE_LEVEL);\n    fill_buffer_with_noise(buffer, rand_gen, normal_dist);\n\n    float mean = 0.0f;\n    for (int c = 0; c < buffer.channel_count(); ++c)\n    {\n        for (float* s = buffer.channel(c); s < buffer.channel(c) + AUDIO_CHUNK_SIZE; ++s)\n        {\n            mean += *s * *s;\n        }\n    }\n    mean /= AUDIO_CHUNK_SIZE * buffer.channel_count();\n    float rms = std::sqrt(mean);\n    ASSERT_NEAR(INPUT_NOISE_LEVEL, rms, 0.002f);\n}\n\nTEST(TestAudioFrontendInternals, TestRampCvOutput)\n{\n    float data_buffer[AUDIO_CHUNK_SIZE];\n    auto target = ramp_cv_output(data_buffer, 1, 0.5);\n    EXPECT_EQ(0.5f, target);\n    float prev = 1.1f;\n    for (auto i : data_buffer)\n    {\n        EXPECT_GT(prev, i);\n        prev = i;\n    }\n}"
  },
  {
    "path": "test/unittests/audio_frontends/portaudio_frontend_test.cpp",
    "content": "#include <array>\n\n#include \"gtest/gtest.h\"\n\n#include \"test_utils/engine_mockup.h\"\n#include \"test_utils/portaudio_mockup.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"audio_frontends/portaudio_frontend.cpp\"\n\nusing ::testing::internal::posix::GetEnv;\nusing ::testing::Return;\nusing ::testing::NiceMock;\nusing ::testing::SetArgPointee;\n\nnamespace sushi::internal::audio_frontend\n{\n\nclass PortaudioFrontendAccessor\n{\npublic:\n    explicit PortaudioFrontendAccessor(PortAudioFrontend& f) : _friend(f) {}\n\n    [[nodiscard]] bool stream_initialized() const\n    {\n        return _friend._stream_initialized;\n    }\n\n    PaStream* stream()\n    {\n        return _friend._stream;\n    };\n\nprivate:\n    PortAudioFrontend& _friend;\n};\n\n}\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::audio_frontend;\nusing namespace sushi::internal::midi_dispatcher;\n\nconstexpr float SAMPLE_RATE = 44100;\n\n\nclass TestPortAudioFrontend : public ::testing::Test\n{\nprotected:\n    TestPortAudioFrontend()\n    = default;\n\n    void SetUp() override\n    {\n        mockPortAudio = std::make_unique<NiceMock<MockPortAudio>>();\n        _module_under_test = std::make_unique<PortAudioFrontend>(&_test_engine);\n\n        _accessor = std::make_unique<PortaudioFrontendAccessor>(*_module_under_test);\n    }\n\n    void TearDown() override\n    {\n        if (_accessor->stream_initialized())\n        {\n            EXPECT_CALL(*mockPortAudio, Pa_IsStreamActive).WillOnce(Return(1));\n            EXPECT_CALL(*mockPortAudio, Pa_StopStream);\n        }\n        EXPECT_CALL(*mockPortAudio, Pa_Terminate);\n        _module_under_test.reset();\n        mockPortAudio.reset();\n    }\n\n    EngineMockup _test_engine {SAMPLE_RATE};\n    std::unique_ptr<PortAudioFrontend> _module_under_test;\n    std::unique_ptr<PortaudioFrontendAccessor> _accessor;\n};\n\nTEST_F(TestPortAudioFrontend, TestInitSuccess)\n{\n    PaError init_value = PaErrorCode::paNoError;\n    int device_count = 2;\n    PaDeviceInfo expected_info;\n    expected_info.maxInputChannels = 2;\n    expected_info.maxOutputChannels = 2;\n    PaStreamInfo stream_info;\n    PortAudioFrontendConfiguration config(0, 1, 0.0f, 0.0f, 1, 1);\n\n    EXPECT_CALL(*mockPortAudio, Pa_Initialize).WillOnce(Return(init_value));\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceCount).WillOnce(Return(device_count));\n    EXPECT_CALL(*mockPortAudio, Pa_GetDefaultInputDevice);\n    EXPECT_CALL(*mockPortAudio, Pa_GetDefaultOutputDevice);\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceInfo(config.input_device_id.value())).WillOnce(Return(const_cast<const PaDeviceInfo*>(&expected_info)));\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceInfo(config.output_device_id.value())).WillOnce(Return(const_cast<const PaDeviceInfo*>(&expected_info)));\n    EXPECT_CALL(*mockPortAudio, Pa_GetStreamInfo(_accessor->stream())).WillOnce(Return(const_cast<const PaStreamInfo*>(&stream_info)));\n    auto ret_code = _module_under_test->init(&config);\n    ASSERT_EQ(AudioFrontendStatus::OK, ret_code);\n}\n\nTEST_F(TestPortAudioFrontend, TestInitFailOnPaInit)\n{\n    PaError init_value = PaErrorCode::paNotInitialized;\n    PortAudioFrontendConfiguration config(0, 1, 0.0f, 0.0f, 1, 1);\n\n    EXPECT_CALL(*mockPortAudio, Pa_Initialize).WillOnce(Return(init_value));\n    auto ret_code = _module_under_test->init(&config);\n    ASSERT_EQ(AudioFrontendStatus::AUDIO_HW_ERROR, ret_code);\n}\n\nTEST_F(TestPortAudioFrontend, TestInitFailGetDeviceCount)\n{\n    PaError init_value = PaErrorCode::paNoError;\n    int device_count = 0;\n    PortAudioFrontendConfiguration config(0, 1, 0.0f, 0.0f, 1, 1);\n\n    EXPECT_CALL(*mockPortAudio, Pa_Initialize).WillOnce(Return(init_value));\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceCount).WillOnce(Return(device_count));\n    auto ret_code = _module_under_test->init(&config);\n    ASSERT_EQ(AudioFrontendStatus::AUDIO_HW_ERROR, ret_code);\n}\n\nTEST_F(TestPortAudioFrontend, TestInitiFailSamplerate)\n{\n    PaError init_value = PaErrorCode::paNoError;\n    int device_count = 2;\n    PaDeviceInfo expected_info;\n    expected_info.maxInputChannels = 2;\n    expected_info.maxOutputChannels = 2;\n    PortAudioFrontendConfiguration config(0, 1, 0.0f, 0.0f, 1, 1);\n\n    EXPECT_CALL(*mockPortAudio, Pa_Initialize).WillOnce(Return(init_value));\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceCount).WillOnce(Return(device_count));\n    EXPECT_CALL(*mockPortAudio, Pa_GetDefaultInputDevice);\n    EXPECT_CALL(*mockPortAudio, Pa_GetDefaultOutputDevice);\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceInfo(config.input_device_id.value())).WillOnce(Return(const_cast<const PaDeviceInfo*>(&expected_info)));\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceInfo(config.output_device_id.value())).WillOnce(Return(const_cast<const PaDeviceInfo*>(&expected_info)));\n    EXPECT_CALL(*mockPortAudio, Pa_IsFormatSupported).WillRepeatedly(Return(PaErrorCode::paInvalidSampleRate));\n    auto ret_code = _module_under_test->init(&config);\n    ASSERT_EQ(AudioFrontendStatus::AUDIO_HW_ERROR, ret_code);\n}\n\nTEST_F(TestPortAudioFrontend, TestInitiFailOpenStream)\n{\n    PaError init_value = PaErrorCode::paNoError;\n    int device_count = 2;\n    PaDeviceInfo expected_info;\n    expected_info.maxInputChannels = 2;\n    expected_info.maxOutputChannels = 2;\n    PortAudioFrontendConfiguration config(0, 1, 0.0f, 0.0f, 1, 1);\n\n    EXPECT_CALL(*mockPortAudio, Pa_Initialize).WillOnce(Return(init_value));\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceCount).WillOnce(Return(device_count));\n    EXPECT_CALL(*mockPortAudio, Pa_GetDefaultInputDevice);\n    EXPECT_CALL(*mockPortAudio, Pa_GetDefaultOutputDevice);\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceInfo(config.input_device_id.value())).WillOnce(Return(const_cast<const PaDeviceInfo*>(&expected_info)));\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceInfo(config.output_device_id.value())).WillOnce(Return(const_cast<const PaDeviceInfo*>(&expected_info)));\n    EXPECT_CALL(*mockPortAudio, Pa_OpenStream).WillOnce(Return(PaErrorCode::paInvalidSampleRate));\n    auto ret_code = _module_under_test->init(&config);\n    ASSERT_EQ(AudioFrontendStatus::AUDIO_HW_ERROR, ret_code);\n}\n\nTEST_F(TestPortAudioFrontend, TestRun)\n{\n    EXPECT_CALL(*mockPortAudio, Pa_StartStream);\n    _module_under_test->run();\n    ASSERT_TRUE(_test_engine.realtime());\n}\n\nTEST_F(TestPortAudioFrontend, TestProcess)\n{\n    PortAudioFrontendConfiguration config(0, 0, 0.0f, 0.0f, 0, 0);\n    int device_count = 1;\n    PaDeviceInfo device_info;\n    device_info.maxInputChannels = 1;\n    device_info.maxOutputChannels = 1;\n    PaStreamInfo stream_info;\n\n\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceCount).WillOnce(Return(device_count));\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceInfo).WillRepeatedly(Return(&device_info));\n    EXPECT_CALL(*mockPortAudio, Pa_GetStreamInfo(_accessor->stream())).WillOnce(Return(const_cast<const PaStreamInfo*>(&stream_info)));\n    auto result = _module_under_test->init(&config);\n    ASSERT_EQ(AudioFrontendStatus::OK, result);\n\n    std::array<float, AUDIO_CHUNK_SIZE> input_data{1.0f};\n    std::array<float, AUDIO_CHUNK_SIZE> output_data{0.0f};\n    PaStreamCallbackTimeInfo time_info;\n    PaStreamCallbackFlags status_flags = 0;\n    PortAudioFrontend::rt_process_callback(static_cast<void*>(input_data.data()),\n                                           static_cast<void*>(output_data.data()),\n                                           AUDIO_CHUNK_SIZE,\n                                           &time_info,\n                                           status_flags,\n                                           static_cast<void*>(_module_under_test.get()));\n    ASSERT_EQ(input_data, output_data);\n    ASSERT_TRUE(_test_engine.process_called);\n}\n\nTEST_F(TestPortAudioFrontend, TestGetDeviceName)\n{\n    auto expected_name = \"a_device\";\n    PaDeviceInfo device_info;\n    device_info.maxInputChannels = 1;\n    device_info.maxOutputChannels = 1;\n    device_info.name = expected_name;\n    device_info.hostApi = 1;\n\n    auto expected_api_name = \"jack\";\n    PaHostApiInfo api_info;\n    api_info.name = expected_api_name;\n\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceInfo)\n    .WillOnce(Return(&device_info))\n    .WillOnce(Return(&device_info))\n    .WillOnce(Return(nullptr));\n\n    EXPECT_CALL(*mockPortAudio, Pa_GetHostApiInfo)\n    .WillOnce(Return(&api_info))\n    .WillOnce(Return(&api_info));\n    \n    std::optional<int> portaudio_output_device_id = 1;\n\n    auto device_name = sushi::internal::audio_frontend::get_portaudio_output_device_name(portaudio_output_device_id);\n\n    ASSERT_EQ(device_name, expected_name); // The specified device\n\n    EXPECT_CALL(*mockPortAudio, Pa_GetDefaultOutputDevice);\n\n    device_name = sushi::internal::audio_frontend::get_portaudio_output_device_name(std::nullopt);\n\n    ASSERT_EQ(device_name, expected_name); // The default device\n\n    device_name = sushi::internal::audio_frontend::get_portaudio_output_device_name(4);\n\n    EXPECT_FALSE(device_name.has_value());\n}\n"
  },
  {
    "path": "test/unittests/control_frontends/osc_frontend_test.cpp",
    "content": "#include <thread>\n\n#include <gtest/gtest.h>\n#include <gmock/gmock.h>\n#include <gmock/gmock-actions.h>\n\n#include \"control_frontends/base_control_frontend.cpp\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_UNUSED_PARAMETER\n\n#include \"test_utils/mock_osc_interface.h\"\n#include \"test_utils/engine_mockup.h\"\n#include \"test_utils/control_mockup.h\"\n#include \"test_utils/mock_processor_container.h\"\n#include \"test_utils/host_control_mockup.h\"\n\n#include \"control_frontends/osc_frontend.cpp\"\n\nELK_POP_WARNING\n\nusing ::testing::Return;\nusing ::testing::StrEq;\nusing ::testing::NiceMock;\nusing ::testing::_;\n\nnamespace sushi::internal::control_frontend\n{\n\nclass OSCFrontendAccessor\n{\npublic:\n    explicit OSCFrontendAccessor(OSCFrontend& f) : _friend(f) {}\n\n    void set_processor_container(const engine::BaseProcessorContainer* container)\n    {\n        _friend._processor_container = container;\n    }\n\n    OscConnection* connect_to_parameter(const std::string& processor_name,\n                                        const std::string& parameter_name,\n                                        ObjectId processor_id,\n                                        ObjectId parameter_id)\n    {\n        return _friend._connect_to_parameter(processor_name, parameter_name, processor_id, parameter_id);\n    }\n\n    OscConnection* connect_to_property(const std::string& processor_name,\n                                       const std::string& property_name,\n                                       ObjectId processor_id,\n                                       ObjectId property_id)\n    {\n        return _friend._connect_to_property(processor_name, property_name, processor_id, property_id);\n    }\n\n    OscConnection* connect_kb_to_track(const Processor* processor)\n    {\n        return _friend._connect_kb_to_track(processor);\n    }\n\n    OscConnection* connect_to_program_change(const Processor* processor)\n    {\n        return _friend._connect_to_program_change(processor);\n    }\n\n    OscConnection* connect_to_bypass_state(const Processor* processor)\n    {\n        return _friend._connect_to_bypass_state(processor);\n    }\n\nprivate:\n    OSCFrontend& _friend;\n};\n\n}\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::control_frontend;\nusing namespace sushi::internal::osc;\n\nconstexpr float TEST_SAMPLE_RATE = 44100;\nconstexpr int OSC_TEST_SERVER_PORT = 24024;\nconstexpr int OSC_TEST_SEND_PORT = 24023;\nconstexpr auto OSC_TEST_SEND_ADDRESS = \"127.0.0.1\";\nconstexpr auto TEST_TRACK_NAME = \"track\";\nconstexpr auto TEST_PROCESSOR_NAME = \"proc\";\n\nclass TestOSCFrontend : public ::testing::Test\n{\nprotected:\n    TestOSCFrontend() = default;\n\n    void SetUp() override\n    {\n        _mock_osc_interface = new MockOscInterface(OSC_TEST_SERVER_PORT, OSC_TEST_SEND_PORT, OSC_TEST_SEND_ADDRESS);\n\n        EXPECT_CALL(*_mock_osc_interface, init()).Times(1).WillOnce(Return(true));\n\n        EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/engine/set_tempo\"), StrEq(\"f\"),\n                                                     OscMethodType::SET_TEMPO, _)).Times(1);\n\n        EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/engine/set_time_signature\"), StrEq(\"ii\"),\n                                                     OscMethodType::SET_TIME_SIGNATURE, _)).Times(1);\n\n        EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/engine/set_playing_mode\"), StrEq(\"s\"),\n                                                     OscMethodType::SET_PLAYING_MODE, _)).Times(1);\n\n        EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/engine/set_sync_mode\"), StrEq(\"s\"),\n                                                     OscMethodType::SET_TEMPO_SYNC_MODE, _)).Times(1);\n\n        EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/engine/set_timing_statistics_enabled\"), StrEq(\"i\"),\n                                                     OscMethodType::SET_TIMING_STATISTICS_ENABLED, _)).Times(1);\n\n        EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/engine/reset_timing_statistics\"), StrEq(\"s\"),\n                                                     OscMethodType::RESET_TIMING_STATISTICS, _)).Times(1);\n\n        EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/engine/reset_timing_statistics\"), StrEq(\"ss\"),\n                                                     OscMethodType::RESET_TIMING_STATISTICS, _)).Times(1);\n\n        EXPECT_CALL(*_mock_osc_interface, run()).Times(1);\n\n        _module_under_test = std::make_unique<OSCFrontend>(&_mock_engine, &_mock_controller, _mock_osc_interface);\n        _test_processor = std::make_shared<DummyProcessor>(_host_control_mockup.make_host_control_mockup());\n        _test_track = std::make_shared<Track>(_host_control_mockup.make_host_control_mockup(), 2, nullptr, true);\n        _test_track->set_name(TEST_TRACK_NAME);\n        _test_processor->set_name(TEST_PROCESSOR_NAME);\n\n        ASSERT_EQ(ControlFrontendStatus::OK, _module_under_test->init());\n\n        _accessor = std::make_unique<OSCFrontendAccessor>(*_module_under_test);\n\n        // Inject the mock container\n        _accessor->set_processor_container(&_mock_processor_container);\n        _module_under_test->run();\n\n        // Set up default returns for mock processor container\n        ON_CALL(_mock_processor_container, all_processors()).WillByDefault(Return(std::vector<std::shared_ptr<const Processor>>({_test_track, _test_processor})));\n        ON_CALL(_mock_processor_container, all_tracks()).WillByDefault(Return(std::vector<std::shared_ptr<const Track>>({_test_track})));\n        ON_CALL(_mock_processor_container, processors_on_track(_test_track->id())).WillByDefault(Return(std::vector<std::shared_ptr<const Processor>>({_test_processor})));\n        ON_CALL(_mock_processor_container, track(::testing::A<const std::string&>())).WillByDefault(Return(_test_track));\n        ON_CALL(_mock_processor_container, track(::testing::A<ObjectId>())).WillByDefault(Return(_test_track));\n        ON_CALL(_mock_processor_container, processor(_test_track->name())).WillByDefault(Return(_test_track));\n        ON_CALL(_mock_processor_container, processor(_test_track->id())).WillByDefault(Return(_test_track));\n        ON_CALL(_mock_processor_container, processor(_test_processor->name())).WillByDefault(Return(_test_processor));\n        ON_CALL(_mock_processor_container, processor(_test_processor->id())).WillByDefault(Return(_test_processor));\n    }\n\n    void TearDown() override\n    {\n        EXPECT_CALL(*_mock_osc_interface, stop()).Times(1);\n\n        EXPECT_CALL(*_mock_osc_interface, delete_method(_)).Times(7);\n\n        _module_under_test->stop();\n    }\n\n    ::testing::NiceMock<MockProcessorContainer> _mock_processor_container;\n    MockOscInterface* _mock_osc_interface {nullptr};\n\n    EngineMockup                  _mock_engine {TEST_SAMPLE_RATE};\n    sushi::control::ControlMockup _mock_controller;\n\n    std::unique_ptr<OSCFrontend> _module_under_test;\n    std::unique_ptr<OSCFrontendAccessor> _accessor;\n\n    HostControlMockup _host_control_mockup;\n    std::shared_ptr<Processor> _test_processor;\n    std::shared_ptr<Track> _test_track;\n};\n\nTEST_F(TestOSCFrontend, TestFailedInit)\n{\n    EXPECT_CALL(*_mock_osc_interface, init()).Times(1).WillOnce(Return(false));\n    ASSERT_EQ(ControlFrontendStatus::INTERFACE_UNAVAILABLE, _module_under_test->init());\n}\n\nTEST_F(TestOSCFrontend, TestConnectFromAllParameters)\n{\n    auto enabled_outputs_empty = _module_under_test->get_enabled_parameter_outputs();\n    EXPECT_EQ(enabled_outputs_empty.size(), 0);\n\n    _module_under_test->connect_from_all_parameters();\n\n    auto enabled_outputs_full = _module_under_test->get_enabled_parameter_outputs();\n\n    EXPECT_EQ(enabled_outputs_full.size(), 5);\n\n    _module_under_test->disconnect_from_all_parameters();\n\n    enabled_outputs_empty = _module_under_test->get_enabled_parameter_outputs();\n    EXPECT_EQ(enabled_outputs_empty.size(), 0);\n}\n\nTEST_F(TestOSCFrontend, TestAddAndRemoveConnectionsForProcessor)\n{\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/bypass/proc\"), StrEq(\"i\"),\n                                                 OscMethodType::SEND_BYPASS_STATE_EVENT, _)).Times(1);\n\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/program/proc\"), StrEq(\"i\"),\n                                                 OscMethodType::SEND_PROGRAM_CHANGE_EVENT, _)).Times(1);\n\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/parameter/proc/param_1\"), StrEq(\"f\"),\n                                                 OscMethodType::SEND_PARAMETER_CHANGE_EVENT, _)).Times(1);\n\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/parameter/proc/gain\"), StrEq(\"f\"),\n                                                 OscMethodType::SEND_PARAMETER_CHANGE_EVENT, _)).Times(1);\n\n    // As this in only done in response to events, test the event handling at the same time\n    ObjectId processor_id = _test_processor->id();\n\n    auto event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_CREATED,\n                                                               processor_id, 0, IMMEDIATE_PROCESS);\n    _module_under_test->process(event.get());\n\n    EXPECT_CALL(*_mock_osc_interface, delete_method(_)).Times(4);\n\n    event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_DELETED,\n                                                          processor_id, 0, IMMEDIATE_PROCESS);\n\n    _module_under_test->process(event.get());\n}\n\nTEST_F(TestOSCFrontend, TestAddAndRemoveConnectionsForTrack)\n{\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/keyboard_event/track\"), StrEq(\"siif\"),\n                                                 OscMethodType::SEND_KEYBOARD_NOTE_EVENT, _)).Times(1);\n\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/keyboard_event/track\"), StrEq(\"sif\"),\n                                                 OscMethodType::SEND_KEYBOARD_MODULATION_EVENT, _)).Times(1);\n\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/bypass/track\"), StrEq(\"i\"),\n                                                 OscMethodType::SEND_BYPASS_STATE_EVENT, _)).Times(1);\n\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/parameter/track/gain\"), StrEq(\"f\"),\n                                                 OscMethodType::SEND_PARAMETER_CHANGE_EVENT, _)).Times(1);\n\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/parameter/track/pan\"), StrEq(\"f\"),\n                                                 OscMethodType::SEND_PARAMETER_CHANGE_EVENT, _)).Times(1);\n\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/parameter/track/mute\"), StrEq(\"f\"),\n                                                 OscMethodType::SEND_PARAMETER_CHANGE_EVENT, _)).Times(1);\n\n    // As this in only done in response to events, test the event handling at the same time\n    ObjectId track_id = _test_track->id();\n\n    auto event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::TRACK_CREATED,\n                                                               track_id, 0, IMMEDIATE_PROCESS);\n    _module_under_test->process(event.get());\n\n    EXPECT_CALL(*_mock_osc_interface, delete_method(_)).Times(6);\n\n    event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::TRACK_DELETED,\n                                                          0, track_id, IMMEDIATE_PROCESS);\n\n    _module_under_test->process(event.get());\n}\n\nTEST_F(TestOSCFrontend, TestConnectParameterChange)\n{\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/parameter/proc/param_1\"), StrEq(\"f\"),\n                                                 OscMethodType::SEND_PARAMETER_CHANGE_EVENT, _)).Times(1);\n\n    auto connection = _accessor->connect_to_parameter(\"proc\", \"param 1\", 1, 2);\n    ASSERT_TRUE(connection != nullptr);\n    EXPECT_EQ(1, connection->processor);\n    EXPECT_EQ(2, connection->parameter);\n}\n\nTEST_F(TestOSCFrontend, TestConnectPropertyChange)\n{\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/property/sampler/sample_file\"), StrEq(\"s\"),\n                                                 OscMethodType::SEND_PROPERTY_CHANGE_EVENT, _)).Times(1);\n\n    auto connection = _accessor->connect_to_property(\"sampler\", \"sample_file\", 1, 2);\n    ASSERT_TRUE(connection != nullptr);\n    EXPECT_EQ(1, connection->processor);\n    EXPECT_EQ(2, connection->parameter);\n}\n\nTEST_F(TestOSCFrontend, TestAddKbdToTrack)\n{\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/keyboard_event/track\"), StrEq(\"siif\"),\n                                                 OscMethodType::SEND_KEYBOARD_NOTE_EVENT, _)).Times(1);\n\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/keyboard_event/track\"), StrEq(\"sif\"),\n                                                 OscMethodType::SEND_KEYBOARD_MODULATION_EVENT, _)).Times(1);\n\n    auto connection = _accessor->connect_kb_to_track(_test_track.get());\n    EXPECT_EQ(_test_track->id(), connection->processor);\n\n    ASSERT_TRUE(connection != nullptr);\n}\n\nTEST_F(TestOSCFrontend, TestConnectProgramChange)\n{\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/program/proc\"), StrEq(\"i\"),\n                                                 OscMethodType::SEND_PROGRAM_CHANGE_EVENT, _)).Times(1);\n\n    auto connection = _accessor->connect_to_program_change(_test_processor.get());\n    ASSERT_TRUE(connection != nullptr);\n    EXPECT_EQ(_test_processor->id(), connection->processor);\n}\n\nTEST_F(TestOSCFrontend, TestSetBypassState)\n{\n    EXPECT_CALL(*_mock_osc_interface, add_method(StrEq(\"/bypass/proc\"), StrEq(\"i\"),\n                                                 OscMethodType::SEND_BYPASS_STATE_EVENT, _)).Times(1);\n\n    auto connection = _accessor->connect_to_bypass_state(_test_processor.get());\n\n    ASSERT_TRUE(connection != nullptr);\n    EXPECT_EQ(_test_processor->id(), connection->processor);\n}\n\nTEST_F(TestOSCFrontend, TestParamChangeNotification)\n{\n    EXPECT_CALL(*_mock_osc_interface, send(StrEq(\"/parameter/proc/param_1\"), testing::Matcher<float>(0.5f))).Times(1);\n\n    ObjectId processor_id = _test_processor->id();\n    ObjectId parameter_id = _test_processor->parameter_from_name(\"param 1\")->id();\n\n    auto event = std::make_unique<ParameterChangeNotificationEvent>(processor_id,\n                                                                    parameter_id,\n                                                                    0.5f,\n                                                                    0.0f,\n                                                                    \"\",\n                                                                    IMMEDIATE_PROCESS);\n\n    _module_under_test->process(event.get()); // Since nothing is connected this should not cause a call.\n\n    _module_under_test->connect_from_all_parameters();\n\n    event = std::make_unique<ParameterChangeNotificationEvent>(processor_id,\n                                                               parameter_id,\n                                                               0.5f,\n                                                               0.0f,\n                                                               \"\",\n                                                               IMMEDIATE_PROCESS);\n\n    _module_under_test->process(event.get()); // But this should - the one expected.\n}\n\n/*TEST_F(TestOSCFrontend, TestPropertyChangeNotification)\n{\n    EXPECT_CALL(*_mock_osc_interface, send(StrEq(\"/parameter/proc/property_1\"), testing::Matcher<float>(0.5f))).Times(1);\n\n    ObjectId processor_id = _test_processor->id();\n    ObjectId parameter_id = _test_processor->parameter_from_name(\"param 1\")->id();\n\n    auto event = ParameterChangeNotificationEvent(ParameterChangeNotificationEvent::Subtype::FLOAT_PARAMETER_CHANGE_NOT,\n                                                  processor_id,\n                                                  parameter_id,\n                                                  0.5f,\n                                                  IMMEDIATE_PROCESS);\n\n    _module_under_test->process(&event); // Since nothing is connected this should not cause a call.\n\n    _module_under_test->connect_from_all_parameters();\n\n    _module_under_test->process(&event); // But this should - the one expected.\n}*/\n\nTEST_F(TestOSCFrontend, TestStateHandling)\n{\n    _module_under_test->set_connect_from_all_parameters(true);\n    _module_under_test->connect_from_all_parameters();\n\n    auto state = _module_under_test->save_state();\n\n    EXPECT_TRUE(state.auto_enable_outputs());\n\n    auto outputs = state.enabled_outputs();\n    ASSERT_EQ(2, outputs.size());\n    ASSERT_EQ(TEST_PROCESSOR_NAME, outputs.front().first);\n    auto params = outputs.front().second;\n    ASSERT_EQ(2, params.size());\n    EXPECT_EQ(0, params.front());\n\n    _module_under_test->disconnect_from_all_parameters();\n    ASSERT_EQ(0, _module_under_test->get_enabled_parameter_outputs().size());\n\n    _module_under_test->set_state(state);\n    auto output_paths = _module_under_test->get_enabled_parameter_outputs();\n    ASSERT_EQ(5, output_paths.size());\n    EXPECT_EQ(\"/parameter/proc/param_1\", output_paths.front());\n}\n\nTEST(TestOSCFrontendInternal, TestMakeSafePath)\n{\n    EXPECT_EQ(\"s_p_a_c_e_\", make_safe_path(\"s p a c e \"));\n    EXPECT_EQ(\"in_valid\", make_safe_path(\"in\\\\\\\" v*[a]{l}id\"));\n}\n"
  },
  {
    "path": "test/unittests/control_frontends/oscpack_osc_messenger_test.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE.  See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include <thread>\n\n#include <gtest/gtest.h>\n#include <gmock/gmock-actions.h>\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n\n// This causes the real oscpack .h files for packet listener and socket,\n// to be excluded in oscpack_osc_messenger.h\n#define OSCPACK_UNIT_TESTS\n\n#include \"test_utils/mock_oscpack.h\"\n#include \"test_utils/control_mockup.h\"\n\n#include \"control_frontends/oscpack_osc_messenger.cpp\"\n\nnamespace sushi::internal::osc\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(OscpackOscMessenger& f) : _friend(f) {}\n\n    [[nodiscard]] OSC_CALLBACK_HANDLE last_generated_handle() const\n    {\n        return _friend._last_generated_handle;\n    }\n\n    const OscpackOscMessenger::RegisteredMessages& registered_messages()\n    {\n        return _friend._registered_messages;\n    }\n\n    void ProcessMessage(const oscpack::ReceivedMessage& m, const IpEndpointName& remoteEndpoint)\n    {\n        _friend.ProcessMessage(m, remoteEndpoint);\n    }\n\n    std::unique_ptr<UdpTransmitSocket>& transmit_socket()\n    {\n        return _friend._transmit_socket;\n    }\n\nprivate:\n    OscpackOscMessenger& _friend;\n};\n\n}\n\nnamespace oscpack = ::osc;\n\nusing ::testing::Return;\nusing ::testing::StrEq;\nusing ::testing::NiceMock;\nusing ::testing::_;\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::control_frontend;\nusing namespace sushi::internal::osc;\n\nconstexpr int OSC_TEST_SERVER_PORT = 24024;\nconstexpr int OSC_TEST_SEND_PORT = 24023;\nconstexpr auto OSC_TEST_SEND_ADDRESS = \"127.0.0.1\";\n\nclass TestOscpackOscMessenger : public ::testing::Test\n{\nprotected:\n    TestOscpackOscMessenger() {}\n\n    void SetUp() override\n    {\n        connection.processor = 0;\n        connection.parameter = 0;\n        connection.controller = &_mock_controller;\n\n        _module_under_test = std::make_unique<OscpackOscMessenger>(OSC_TEST_SERVER_PORT,\n                                                                   OSC_TEST_SEND_PORT,\n                                                                   OSC_TEST_SEND_ADDRESS);\n\n        _accessor = std::make_unique<sushi::internal::osc::Accessor>(*_module_under_test);\n\n        _module_under_test->init();\n    }\n\n    sushi::control::ControlMockup _mock_controller;\n\n    OscConnection connection;\n\n    IpEndpointName _endpoint{\"\", 0}; // Just to match ProcessMessage signature - the endpoint is unused.\n\n    char _buffer[OSC_OUTPUT_BUFFER_SIZE];\n\n    std::unique_ptr<OscpackOscMessenger> _module_under_test;\n    std::unique_ptr<sushi::internal::osc::Accessor> _accessor;\n};\n\nTEST_F(TestOscpackOscMessenger, TestAddAndRemoveConnections)\n{\n    EXPECT_EQ(_accessor->last_generated_handle(), 0);\n    EXPECT_EQ(_accessor->registered_messages().size(), 0);\n\n    auto id_1 = _module_under_test->add_method(\"/engine/set_tempo\",\n                                              \"f\", sushi::internal::osc::OscMethodType::SET_TEMPO,\n                                              &connection);\n\n    EXPECT_EQ(_accessor->last_generated_handle(), 1);\n    EXPECT_EQ(_accessor->registered_messages().size(), 1);\n    EXPECT_EQ(reinterpret_cast<OSC_CALLBACK_HANDLE>(id_1), 0);\n\n    // Attempting to register with an existing address_pattern and tts should fail:\n    auto id_2 = _module_under_test->add_method(\"/engine/set_tempo\",\n                                               \"f\", sushi::internal::osc::OscMethodType::SET_TEMPO,\n                                               &connection);\n\n    EXPECT_EQ(_accessor->last_generated_handle(), 1);\n    EXPECT_EQ(_accessor->registered_messages().size(), 1);\n    EXPECT_EQ(reinterpret_cast<OSC_CALLBACK_HANDLE>(id_2), -1);\n\n    // BUT the same AP with a different TTS, should be fine:\n    auto id_3 = _module_under_test->add_method(\"/engine/set_tempo\",\n                                               \"ff\", sushi::internal::osc::OscMethodType::SET_TEMPO,\n                                               &connection);\n\n    EXPECT_EQ(_accessor->last_generated_handle(), 2);\n    EXPECT_EQ(_accessor->registered_messages().size(), 2);\n    EXPECT_EQ(reinterpret_cast<OSC_CALLBACK_HANDLE>(id_3), 1);\n\n    // Calling with an unused ID should not remove anything:\n    OSC_CALLBACK_HANDLE unused_id = 1234;\n    _module_under_test->delete_method(reinterpret_cast<void*>(unused_id));\n    EXPECT_EQ(_accessor->registered_messages().size(), 2);\n\n    // Calling delete_method with the returned ID should work:\n    _module_under_test->delete_method(id_1);\n    EXPECT_EQ(_accessor->registered_messages().size(), 1);\n}\n\nTEST_F(TestOscpackOscMessenger, TestSendParameterChange)\n{\n    auto address_pattern = \"/parameter/track_1/param_1\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"f\", sushi::internal::osc::OscMethodType::SEND_PARAMETER_CHANGE_EVENT,\n                                   &connection);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << 0.5f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n\n\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.parameter_controller_mockup()->get_args_from_last_call();\n    EXPECT_EQ(0, std::stoi(args[\"processor id\"]));\n    EXPECT_EQ(0, std::stoi(args[\"parameter id\"]));\n    EXPECT_FLOAT_EQ(0.5f, std::stof(args[\"value\"]));\n\n    // Test with a path not registered\n    _mock_controller.clear_recent_call();\n    oscpack::OutboundPacketStream p2(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p2 << oscpack::BeginMessage(\"/parameter/sampler/attack\") << 5.0f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message2(oscpack::ReceivedPacket(p2.Data(), p2.Size()));\n    _accessor->ProcessMessage(message2, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    ASSERT_FALSE(_mock_controller.was_recently_called());\n}\n\nTEST_F(TestOscpackOscMessenger, TestSendPropertyChange)\n{\n    auto address_pattern = \"/property/sampler/sample_file\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"s\", sushi::internal::osc::OscMethodType::SEND_PROPERTY_CHANGE_EVENT,\n                                   &connection);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << \"Sample file\" << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.parameter_controller_mockup()->get_args_from_last_call();\n    EXPECT_EQ(0, std::stoi(args[\"processor id\"]));\n    EXPECT_EQ(0, std::stoi(args[\"property id\"]));\n    EXPECT_EQ(\"Sample file\", args[\"value\"]);\n\n    // Test with a path not registered\n    _mock_controller.clear_recent_call();\n    oscpack::OutboundPacketStream p2(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p2 << oscpack::BeginMessage(\"/property/sampler/attack\") << 4 << oscpack::EndMessage;\n    oscpack::ReceivedMessage message2(oscpack::ReceivedPacket(p2.Data(), p2.Size()));\n    _accessor->ProcessMessage(message2, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    ASSERT_FALSE(_mock_controller.was_recently_called());\n}\n\nTEST_F(TestOscpackOscMessenger, TestSendNoteOn)\n{\n    auto address_pattern = \"/keyboard_event/sampler\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"siif\", sushi::internal::osc::OscMethodType::SEND_KEYBOARD_NOTE_EVENT,\n                                   &connection);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << \"note_on\" << 0 << 46 << 0.8f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.keyboard_controller_mockup()->get_args_from_last_call();\n    EXPECT_EQ(0, std::stoi(args[\"track id\"]));\n    EXPECT_EQ(0, std::stoi(args[\"channel\"]));\n    EXPECT_EQ(46, std::stoi(args[\"note\"]));\n    EXPECT_FLOAT_EQ(0.8f, std::stof(args[\"velocity\"]));\n\n    // Test with a path not registered\n    _mock_controller.clear_recent_call();\n    oscpack::OutboundPacketStream p2(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p2 << oscpack::BeginMessage(\"/keyboard_event/drums\") << \"note_on\" << 4 << 20 << 0.2f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message2(oscpack::ReceivedPacket(p2.Data(), p2.Size()));\n    _accessor->ProcessMessage(message2, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    ASSERT_FALSE(_mock_controller.was_recently_called());\n}\n\nTEST_F(TestOscpackOscMessenger, TestSendNoteOff)\n{\n    auto address_pattern = \"/keyboard_event/sampler\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"siif\", sushi::internal::osc::OscMethodType::SEND_KEYBOARD_NOTE_EVENT,\n                                   &connection);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << \"note_off\" << 1 << 52 << 0.7f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.keyboard_controller_mockup()->get_args_from_last_call();\n    EXPECT_EQ(0, std::stoi(args[\"track id\"]));\n    EXPECT_EQ(1, std::stoi(args[\"channel\"]));\n    EXPECT_EQ(52, std::stoi(args[\"note\"]));\n    EXPECT_FLOAT_EQ(0.7f, std::stof(args[\"velocity\"]));\n\n    // Test with a path not registered\n    _mock_controller.clear_recent_call();\n    oscpack::OutboundPacketStream p2(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p2 << oscpack::BeginMessage(\"/keyboard_event/drums\") << \"note_off\" << 4 << 20 << 0.2f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message2(oscpack::ReceivedPacket(p2.Data(), p2.Size()));\n    _accessor->ProcessMessage(message2, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    ASSERT_FALSE(_mock_controller.was_recently_called());\n}\n\nTEST_F(TestOscpackOscMessenger, TestSendNoteAftertouch)\n{\n    auto address_pattern = \"/keyboard_event/sampler\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"siif\", sushi::internal::osc::OscMethodType::SEND_KEYBOARD_NOTE_EVENT,\n                                   &connection);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << \"note_aftertouch\" << 10 << 36 << 0.1f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.keyboard_controller_mockup()->get_args_from_last_call();\n    EXPECT_EQ(0, std::stoi(args[\"track id\"]));\n    EXPECT_EQ(10, std::stoi(args[\"channel\"]));\n    EXPECT_EQ(36, std::stoi(args[\"note\"]));\n    EXPECT_FLOAT_EQ(0.1f, std::stof(args[\"value\"]));\n\n    // Test with a path not registered\n    _mock_controller.clear_recent_call();\n    oscpack::OutboundPacketStream p2(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p2 << oscpack::BeginMessage(\"/keyboard_event/drums\") << \"note_aftertouch\" << 4 << 20 << 0.2f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message2(oscpack::ReceivedPacket(p2.Data(), p2.Size()));\n    _accessor->ProcessMessage(message2, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    ASSERT_FALSE(_mock_controller.was_recently_called());\n}\n\nTEST_F(TestOscpackOscMessenger, TestSendKeyboardModulation)\n{\n    auto address_pattern = \"/keyboard_event/sampler\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"sif\",\n                                   sushi::internal::osc::OscMethodType::SEND_KEYBOARD_MODULATION_EVENT,\n                                   &connection);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << \"modulation\" << 9 << 0.5f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.keyboard_controller_mockup()->get_args_from_last_call();\n    EXPECT_EQ(0, std::stoi(args[\"track id\"]));\n    EXPECT_EQ(9, std::stoi(args[\"channel\"]));\n    EXPECT_FLOAT_EQ(0.5f, std::stof(args[\"value\"]));\n\n    // Test with a path not registered\n    _mock_controller.clear_recent_call();\n    oscpack::OutboundPacketStream p2(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p2 << oscpack::BeginMessage(\"/keyboard_event/drums\") << \"modulation\" << 4 << 0.2f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message2(oscpack::ReceivedPacket(p2.Data(), p2.Size()));\n    _accessor->ProcessMessage(message2, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    ASSERT_FALSE(_mock_controller.was_recently_called());\n}\n\nTEST_F(TestOscpackOscMessenger, TestSendKeyboardPitchBend)\n{\n    auto address_pattern = \"/keyboard_event/sampler\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"sif\",\n                                 sushi::internal::osc::OscMethodType::SEND_KEYBOARD_MODULATION_EVENT,\n                                   &connection);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << \"pitch_bend\" << 3 << 0.3f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.keyboard_controller_mockup()->get_args_from_last_call();\n    EXPECT_EQ(0, std::stoi(args[\"track id\"]));\n    EXPECT_EQ(3, std::stoi(args[\"channel\"]));\n    EXPECT_FLOAT_EQ(0.3f, std::stof(args[\"value\"]));\n\n    // Test with a path not registered\n    _mock_controller.clear_recent_call();\n    oscpack::OutboundPacketStream p2(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p2 << oscpack::BeginMessage(\"/keyboard_event/drums\") << \"pitch_bend\" << 1 << 0.2f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message2(oscpack::ReceivedPacket(p2.Data(), p2.Size()));\n    _accessor->ProcessMessage(message2, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    ASSERT_FALSE(_mock_controller.was_recently_called());\n}\n\nTEST_F(TestOscpackOscMessenger, TestSendKeyboardAftertouch)\n{\n    auto address_pattern = \"/keyboard_event/sampler\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"sif\",\n                                 sushi::internal::osc::OscMethodType::SEND_KEYBOARD_MODULATION_EVENT,\n                                   &connection);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << \"aftertouch\" << 11 << 0.11f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.keyboard_controller_mockup()->get_args_from_last_call();\n    EXPECT_EQ(0, std::stoi(args[\"track id\"]));\n    EXPECT_EQ(11, std::stoi(args[\"channel\"]));\n    EXPECT_FLOAT_EQ(0.11f, std::stof(args[\"value\"]));\n\n    // Test with a path not registered\n    _mock_controller.clear_recent_call();\n    oscpack::OutboundPacketStream p2(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p2 << oscpack::BeginMessage(\"/keyboard_event/drums\") << \"aftertouch\" << 12 << 0.52f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message2(oscpack::ReceivedPacket(p2.Data(), p2.Size()));\n    _accessor->ProcessMessage(message2, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    ASSERT_FALSE(_mock_controller.was_recently_called());\n}\n\nTEST_F(TestOscpackOscMessenger, TestSendProgramChange)\n{\n    auto address_pattern = \"/program/sampler\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"i\", sushi::internal::osc::OscMethodType::SEND_PROGRAM_CHANGE_EVENT,\n                                   &connection);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << 1 << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.program_controller_mockup()->get_args_from_last_call();\n    EXPECT_EQ(0, std::stoi(args[\"processor id\"]));\n    EXPECT_EQ(1, std::stoi(args[\"program id\"]));\n\n    // Test with a path not registered\n    _mock_controller.clear_recent_call();\n    oscpack::OutboundPacketStream p2(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p2 << oscpack::BeginMessage(\"/program/drums\") << 10 << oscpack::EndMessage;\n    oscpack::ReceivedPacket received_packet2(p2.Data(), p2.Size());\n    oscpack::ReceivedMessage message2(received_packet2);\n    _accessor->ProcessMessage(message2, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    ASSERT_FALSE(_mock_controller.was_recently_called());\n}\n\nTEST_F(TestOscpackOscMessenger, TestSetBypassState)\n{\n    auto address_pattern = \"/bypass/sampler\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"i\", sushi::internal::osc::OscMethodType::SEND_BYPASS_STATE_EVENT,\n                                   &connection);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << 1 << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.audio_graph_controller_mockup()->get_args_from_last_call();\n    EXPECT_EQ(0, std::stoi(args[\"processor id\"]));\n    EXPECT_EQ(1, std::stoi(args[\"bypass enabled\"]));\n\n    // Test with a path not registered\n    _mock_controller.clear_recent_call();\n    oscpack::OutboundPacketStream p2(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p2 << oscpack::BeginMessage(\"/bypass/drums\") << 1 << oscpack::EndMessage;\n    oscpack::ReceivedMessage message2(oscpack::ReceivedPacket(p2.Data(), p2.Size()));\n    _accessor->ProcessMessage(message2, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    ASSERT_FALSE(_mock_controller.was_recently_called());\n}\n\nTEST_F(TestOscpackOscMessenger, TestSetTempo)\n{\n    auto address_pattern = \"/engine/set_tempo\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"f\", sushi::internal::osc::OscMethodType::SET_TEMPO,\n                                   &_mock_controller);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << 136.0f << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.transport_controller_mockup()->get_args_from_last_call();\n\n    ASSERT_TRUE(_mock_controller.was_recently_called());\n\n    EXPECT_EQ(136.0f, std::stoi(args[\"tempo\"]));\n}\n\nTEST_F(TestOscpackOscMessenger, TestSetTimeSignature)\n{\n    auto address_pattern = \"/engine/set_time_signature\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"ii\", sushi::internal::osc::OscMethodType::SET_TIME_SIGNATURE,\n                                   &_mock_controller);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << 7 << 8 << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.transport_controller_mockup()->get_args_from_last_call();\n\n    ASSERT_TRUE(_mock_controller.was_recently_called());\n\n    EXPECT_EQ(7, std::stoi(args[\"numerator\"]));\n    EXPECT_EQ(8, std::stoi(args[\"denominator\"]));\n}\n\nTEST_F(TestOscpackOscMessenger, TestSetPlayingMode)\n{\n    auto address_pattern = \"/engine/set_playing_mode\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"s\", sushi::internal::osc::OscMethodType::SET_PLAYING_MODE,\n                                   &_mock_controller);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << \"playing\" << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.transport_controller_mockup()->get_args_from_last_call();\n\n    ASSERT_TRUE(_mock_controller.was_recently_called());\n\n    EXPECT_EQ(\"PLAYING\", args[\"playing mode\"]);\n}\n\nTEST_F(TestOscpackOscMessenger, TestSetSyncMode)\n{\n    auto address_pattern = \"/engine/set_sync_mode\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"s\", sushi::internal::osc::OscMethodType::SET_TEMPO_SYNC_MODE,\n                                   &_mock_controller);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << \"midi\" << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.transport_controller_mockup()->get_args_from_last_call();\n\n    ASSERT_TRUE(_mock_controller.was_recently_called());\n\n    EXPECT_EQ(\"MIDI\", args[\"sync mode\"]);\n}\n\nTEST_F(TestOscpackOscMessenger, TestSetTimingStatisticsEnabled)\n{\n    auto address_pattern = \"/engine/set_timing_statistics_enabled\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"i\",\n                                 sushi::internal::osc::OscMethodType::SET_TIMING_STATISTICS_ENABLED,\n                                   &_mock_controller);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << 1 << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    auto args = _mock_controller.timing_controller_mockup()->get_args_from_last_call();\n\n    ASSERT_TRUE(_mock_controller.was_recently_called());\n\n    EXPECT_EQ(\"1\", args[\"enabled\"]);\n}\n\nTEST_F(TestOscpackOscMessenger, TestResetAllTimings)\n{\n    auto address_pattern = \"/engine/reset_timing_statistics\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"s\",\n                                 sushi::internal::osc::OscMethodType::RESET_TIMING_STATISTICS,\n                                   &_mock_controller);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << \"all\" << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    ASSERT_TRUE(_mock_controller.was_recently_called());\n}\n\nTEST_F(TestOscpackOscMessenger, TestResetProcessorTimings)\n{\n    auto address_pattern = \"/engine/reset_timing_statistics\";\n\n    _module_under_test->add_method(address_pattern,\n                                   \"ss\",\n                                 sushi::internal::osc::OscMethodType::RESET_TIMING_STATISTICS,\n                                   &_mock_controller);\n\n    oscpack::OutboundPacketStream p(_buffer, OSC_OUTPUT_BUFFER_SIZE);\n    p << oscpack::BeginMessage(address_pattern) << \"processor\" << \"sampler\" << oscpack::EndMessage;\n    oscpack::ReceivedMessage message(oscpack::ReceivedPacket(p.Data(), p.Size()));\n\n    _accessor->ProcessMessage(message, reinterpret_cast<const IpEndpointName&>(_endpoint));\n\n    ASSERT_TRUE(_mock_controller.was_recently_called());\n\n    auto args = _mock_controller.timing_controller_mockup()->get_args_from_last_call();\n    EXPECT_EQ(0, std::stoi(args[\"processor_id\"]));\n}\n\nTEST_F(TestOscpackOscMessenger, TestSendFloat)\n{\n    auto address_pattern = \"/an/osc/message\";\n\n    EXPECT_CALL(*(_accessor->transmit_socket().get()), Send(_, _)).Times(1);\n\n    _module_under_test->send(address_pattern, 0.5f);\n}\n\nTEST_F(TestOscpackOscMessenger, TestSendInt)\n{\n    auto address_pattern = \"/an/osc/message\";\n\n    EXPECT_CALL(*(_accessor->transmit_socket().get()), Send(_, _)).Times(1);\n\n    _module_under_test->send(address_pattern, 5);\n}\n"
  },
  {
    "path": "test/unittests/dsp_library/envelope_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"dsp_library/envelopes.h\"\n\n\nusing namespace sushi::dsp;\n\n/* Test the envelope class */\nclass TestADSREnvelope : public ::testing::Test\n{\nprotected:\n    TestADSREnvelope() = default;\n\n    void SetUp() override\n    {\n        _module_under_test.set_samplerate(100);\n        _module_under_test.set_parameters(1, 1, 0.5, 1);\n    }\n\n    AdsrEnvelope _module_under_test;\n};\n\nTEST_F(TestADSREnvelope, TestNormalOperation)\n{\n    EXPECT_TRUE(_module_under_test.finished());\n    _module_under_test.gate(true);\n    EXPECT_FALSE(_module_under_test.finished());\n\n    /* Test Attack phase */\n    float level = _module_under_test.tick(50);\n    EXPECT_NEAR(0.5f, level, 0.001);\n    level = _module_under_test.tick(50);\n    /* this should be around the maximum peak */\n    EXPECT_NEAR(1.0f, level, 0.001);\n    /* Move into the decay phase */\n    level = _module_under_test.tick(50);\n    EXPECT_NEAR(0.75f, level, 0.001);\n    /* Now we should be in the sustain phase */\n    level = _module_under_test.tick(200);\n    EXPECT_FLOAT_EQ(0.5f, level);\n\n    /* Set gate off and go to decay phase */\n    _module_under_test.gate(false);\n    level = _module_under_test.tick(50);\n    EXPECT_NEAR(0.25f, level, 0.001);\n    level = _module_under_test.tick(55);\n    EXPECT_FLOAT_EQ(0.0f, level);\n    EXPECT_TRUE(_module_under_test.finished());\n}\n\nTEST_F(TestADSREnvelope, TestParameterLimits)\n{\n    _module_under_test.set_parameters(0, 0, 0.5f, 0);\n    EXPECT_TRUE(_module_under_test.finished());\n    _module_under_test.gate(true);\n    EXPECT_FALSE(_module_under_test.finished());\n    /* Only 1 state transition per tick, so tick is called twice */\n    float level = _module_under_test.tick(2);\n    level = _module_under_test.tick(2);\n    EXPECT_FLOAT_EQ(0.5f, level);\n\n    /* Reset and test with tick(0) */\n    _module_under_test.reset();\n    level = _module_under_test.tick(0);\n    EXPECT_FLOAT_EQ(0.0f, level);\n    EXPECT_FLOAT_EQ(0.0f, _module_under_test.level());\n}\n"
  },
  {
    "path": "test/unittests/dsp_library/master_limiter_test.cpp",
    "content": "#include <array>\n\n#include \"gtest/gtest.h\"\n\n#include \"dsp_library/master_limiter.h\"\n#include \"test/data/master_limiter_test_data.h\"\n\nusing namespace sushi::dsp;\n\nclass TestUpSampler : public ::testing::Test\n{\nprotected:\n    TestUpSampler() = default;\n    void SetUp() override\n    {\n        _module_under_test.reset();\n    }\n\n    UpSampler<UPSAMPLING_TEST_DATA_SIZE> _module_under_test;\n};\n\nTEST_F(TestUpSampler, UpSampling)\n{\n    std::array<float, UPSAMPLING_TEST_DATA4X_SIZE> out;\n    _module_under_test.process(UPSAMPLING_TEST_DATA, out.data());\n    for (size_t i = 0; i < UPSAMPLING_TEST_DATA4X_SIZE; i++)\n    {\n        EXPECT_FLOAT_EQ(UPSAMPLING_TEST_DATA4X[i], out[i]);\n    }\n}\n\nconstexpr float TEST_SAMPLERATE = 48000.0f;\nconstexpr float TEST_RELEASE_TIME_MS = 100.0f;\nconstexpr float TEST_ATTACK_TIME_MS = 50.0f;\n\nclass TestMasterLimiter : public ::testing::Test\n{\nprotected:\n    TestMasterLimiter() = default;\n    void SetUp() override\n    {\n        _module_under_test.init(TEST_SAMPLERATE);\n    }\n\n    MasterLimiter<LIMITER_INPUT_DATA_SIZE> _module_under_test{TEST_RELEASE_TIME_MS, TEST_ATTACK_TIME_MS};\n};\n\nTEST_F(TestMasterLimiter, Limit)\n{\n    float out[LIMITER_OUTPUT_DATA_SIZE];\n    _module_under_test.process(LIMITER_INPUT_DATA, out);\n    for (int i = 0; i < LIMITER_OUTPUT_DATA_SIZE; i++)\n    {\n        EXPECT_NEAR(1.0, out[i] / LIMITER_OUTPUT_DATA[i], 1e-6);\n    }\n}"
  },
  {
    "path": "test/unittests/dsp_library/sample_wrapper_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"test_utils/test_utils.h\"\n#include \"dsp_library/sample_wrapper.h\"\n\nusing namespace dsp;\n\nconst float SAMPLE_DATA[] = {1.0f, 2.0f, 2.0f, 1.0f, 1.0f};\nconst int SAMPLE_DATA_LENGTH = sizeof(SAMPLE_DATA) / sizeof(float);\n\nclass TestSampleWrapper : public ::testing::Test\n{\nprotected:\n    TestSampleWrapper() = default;\n\n    Sample _module_under_test{SAMPLE_DATA, SAMPLE_DATA_LENGTH};\n};\n\nTEST_F(TestSampleWrapper, TestSampleInterpolation)\n{\n    // Get exact sample values\n    EXPECT_FLOAT_EQ(1.0f, _module_under_test.at(0.0f));\n    EXPECT_FLOAT_EQ(2.0f, _module_under_test.at(1.0f));\n    // Get interpolated values\n    EXPECT_FLOAT_EQ(1.5f, _module_under_test.at(2.5f));\n}\n"
  },
  {
    "path": "test/unittests/dsp_library/value_smoother_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"dsp_library/value_smoother.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\n\nconstexpr float TEST_SAMPLE_RATE = 1000;\nconstexpr float TEST_TARGET_VALUE = 1;\n\ntemplate <typename T, int mode>\nvoid test_common(ValueSmoother<T, mode>& module_under_test)\n{\n    module_under_test.set_direct(2.0);\n    EXPECT_FLOAT_EQ(2.0, module_under_test.value());\n    EXPECT_TRUE(module_under_test.stationary());\n    EXPECT_FLOAT_EQ(2.0, module_under_test.next_value());\n\n    module_under_test.set(TEST_TARGET_VALUE);\n    EXPECT_FLOAT_EQ(2.0, module_under_test.value());\n    EXPECT_FALSE(module_under_test.stationary());\n\n    for (int i = 0; i < 5; ++i)\n    {\n        auto val = module_under_test.value();\n        EXPECT_LT(module_under_test.next_value(), val);\n    }\n}\n\nclass ValueSmootherTest : public ::testing::Test\n{\nprotected:\n    ValueSmootherTest() = default;\n\n    void SetUp() override\n    {\n        _module_under_test_filter.set_lag_time(std::chrono::milliseconds(5), TEST_SAMPLE_RATE);\n        _module_under_test_ramp.set_lag_time(std::chrono::milliseconds(5), TEST_SAMPLE_RATE);\n        _module_under_test_exp_ramp.set_lag_time(std::chrono::milliseconds(5), TEST_SAMPLE_RATE);\n    }\n    ValueSmootherFilter<float> _module_under_test_filter;\n    ValueSmootherRamp<float> _module_under_test_ramp;\n    ValueSmootherExpRamp<float> _module_under_test_exp_ramp;\n };\n\n\nTEST_F(ValueSmootherTest, TestLinearFloat)\n{\n    test_common(_module_under_test_ramp);\n\n    EXPECT_TRUE(_module_under_test_ramp.stationary());\n    EXPECT_FLOAT_EQ(TEST_TARGET_VALUE, _module_under_test_ramp.value());\n}\n\nTEST_F(ValueSmootherTest, TestExpFloat)\n{\n    test_common(_module_under_test_filter);\n    /* As the filter version approaches the target value asymptotically, it needs\n     * to run a few more cycles before the value comes close enough */\n    for (int i = 0; i < 25; ++i)\n    {\n        _module_under_test_filter.next_value();\n    }\n    EXPECT_TRUE(_module_under_test_filter.stationary());\n    EXPECT_NEAR(TEST_TARGET_VALUE, _module_under_test_filter.value(), 0.001);\n}\n\n\nTEST_F(ValueSmootherTest, TestExponentialFloat)\n{\n    test_common(_module_under_test_exp_ramp);\n\n    EXPECT_TRUE(_module_under_test_exp_ramp.stationary());\n    EXPECT_FLOAT_EQ(TEST_TARGET_VALUE, _module_under_test_exp_ramp.value());\n}"
  },
  {
    "path": "test/unittests/engine/audio_graph_test.cpp",
    "content": "#include <thread>\n\n#include \"gtest/gtest.h\"\n\n#include \"engine/transport.h\"\n\n#include \"engine/audio_graph.cpp\"\n#include \"test_utils/host_control_mockup.h\"\n#include \"test_utils/audio_graph_accessor.h\"\n\nconstexpr float SAMPLE_RATE = 44000;\nconstexpr int TEST_MAX_TRACKS = 2;\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\n\n#if defined(_MSC_VER)\n#define DISABLE_MULTICORE_UNIT_TESTS\n#endif\n\n\nclass TestAudioGraph : public ::testing::Test\n{\nprotected:\n    using ::testing::Test::SetUp; // Hide error of hidden overload of virtual function in clang when signatures differ but the name is the same\n    TestAudioGraph() = default;\n\n    void SetUp(int cores)\n    {\n        _module_under_test = std::make_unique<AudioGraph>(cores, TEST_MAX_TRACKS, &_timer, SAMPLE_RATE, \"\");\n        _accessor = std::make_unique<AudioGraphAccessor>(*_module_under_test);\n        _timer.enable(true);\n    }\n\n    HostControlMockup _hc;\n    std::unique_ptr<AudioGraph> _module_under_test;\n\n    std::unique_ptr<AudioGraphAccessor> _accessor;\n\n    performance::PerformanceTimer _timer;\n\n    Track _track_1 {_hc.make_host_control_mockup(SAMPLE_RATE), 2, &_timer};\n    Track _track_2 {_hc.make_host_control_mockup(SAMPLE_RATE), 2, &_timer};\n};\n\nTEST_F(TestAudioGraph, TestSingleCoreOperation)\n{\n    SetUp(1);\n    ASSERT_TRUE(_module_under_test->add(&_track_1));\n    ASSERT_TRUE(_module_under_test->add(&_track_2));\n\n    ASSERT_EQ(1u, _accessor->audio_graph().size());\n    ASSERT_EQ(2u, _accessor->audio_graph()[0].tracks.size());\n\n    _module_under_test->render();\n\n    ASSERT_TRUE(_module_under_test->remove(&_track_1));\n    ASSERT_TRUE(_module_under_test->remove(&_track_2));\n    ASSERT_FALSE(_module_under_test->remove(&_track_2));\n\n    ASSERT_EQ(0u, _accessor->audio_graph()[0].tracks.size());\n\n    ASSERT_EQ(1, _module_under_test->threads());\n\n    _timer.enable(false);\n    // In single core operation, the audio graph should not record any timing statistics\n    ASSERT_FALSE(_timer.timings_for_node(-2).has_value());\n}\n\n/**\n * On Apple computers, if Twine is built with CoreAudio support, this unit test will fail,\n * since it fails to join a real-time thread workgroup - since there isn't one.\n * To re-enable the test, we'll first need to build Twine statically only for these test,\n * and either mock CoreAudio, or disable it in this static Twine build for these tests.\n *\n * It can also fail on Linux in other cases (e.g. sometimes when running in a Dockerized container).\n */\n#ifndef DISABLE_MULTICORE_UNIT_TESTS\nTEST_F(TestAudioGraph, TestMultiCoreOperation)\n{\n    SetUp(3);\n    ASSERT_TRUE(_module_under_test->add(&_track_1));\n    ASSERT_TRUE(_module_under_test->add(&_track_2));\n\n    ASSERT_EQ(3, _module_under_test->threads());\n\n    // Tracks should end up in slot 0 and 1\n    ASSERT_EQ(3u, _accessor->audio_graph().size());\n    ASSERT_EQ(1u, _accessor->audio_graph()[0].tracks.size());\n    ASSERT_EQ(1u, _accessor->audio_graph()[1].tracks.size());\n    ASSERT_EQ(0u, _accessor->audio_graph()[2].tracks.size());\n\n    auto event = RtEvent::make_note_on_event(_track_1.id(), 0, 0, 48, 1.0f);\n    _track_1.process_event(event);\n    _track_2.process_event(event);\n    _module_under_test->render();\n\n    // Test that events were properly passed through\n    auto queues = _module_under_test->event_outputs();\n    EXPECT_EQ(1, queues[0].size());\n    EXPECT_EQ(1, queues[1].size());\n    EXPECT_EQ(0, queues[2].size());\n\n    _timer.enable(false);\n    // Thread ids should be counted backwards from -2 (since -1 is the full engine and >0 is for tracks and processors)\n    ASSERT_TRUE(_timer.timings_for_node(-2).has_value());\n    ASSERT_TRUE(_timer.timings_for_node(-3).has_value());\n    ASSERT_TRUE(_timer.timings_for_node(-4).has_value());\n    ASSERT_FALSE(_timer.timings_for_node(-5).has_value());\n}\n#endif\n\nTEST_F(TestAudioGraph, TestMaxNumberOfTracks)\n{\n    SetUp(1);\n    ASSERT_TRUE(_module_under_test->add(&_track_1));\n    ASSERT_TRUE(_module_under_test->add(&_track_2));\n    ASSERT_FALSE(_module_under_test->add(&_track_2));\n\n    ASSERT_EQ(1u, _accessor->audio_graph().size());\n    ASSERT_EQ(2u, _accessor->audio_graph()[0].tracks.size());\n}\n"
  },
  {
    "path": "test/unittests/engine/controller_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"engine/audio_engine.h\"\n#include \"engine/json_configurator.h\"\n#include \"engine/controller/controller.cpp\"\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/audio_frontend_mockup.h\"\n\n#include \"sushi/utils.h\"\n\n/* Currently testing Controller as a complete class\n * eventually we might want to test the individual\n * controller interfaces separately */\n#include \"engine/controller/system_controller.cpp\"\n#include \"engine/controller/transport_controller.cpp\"\n#include \"engine/controller/timing_controller.cpp\"\n#include \"engine/controller/keyboard_controller.cpp\"\n#include \"engine/controller/parameter_controller.cpp\"\n#include \"engine/controller/program_controller.cpp\"\n#include \"engine/controller/cv_gate_controller.cpp\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\n\nconstexpr unsigned int TEST_SAMPLE_RATE = 48000;\nconstexpr unsigned int ENGINE_CHANNELS = 8;\nconst std::string TEST_FILE = \"config.json\";\n\nclass ControllerTest : public ::testing::Test\n{\nprotected:\n    ControllerTest() = default;\n\n    void SetUp() override\n    {\n        _engine.set_audio_channels(ENGINE_CHANNELS, ENGINE_CHANNELS);\n\n        ASSERT_EQ(jsonconfig::JsonConfigReturnStatus::OK, _configurator.load_host_config());\n        ASSERT_EQ(jsonconfig::JsonConfigReturnStatus::OK, _configurator.load_tracks());\n\n        _module_under_test = std::make_unique<Controller>(&_engine, &_midi_dispatcher, &_audio_frontend);\n        ChunkSampleBuffer buffer(8);\n        ControlBuffer ctrl_buffer;\n        // Run once so that pending changes are executed\n        _engine.process_chunk(&buffer, &buffer, &ctrl_buffer, &ctrl_buffer, Time(0), 0);\n        ASSERT_TRUE(_module_under_test != nullptr);\n    }\n\n    std::string _path{test_utils::get_data_dir_path() + TEST_FILE};\n    std::string _json_data{ sushi::read_file(_path).value()};\n    AudioEngine _engine{TEST_SAMPLE_RATE};\n    midi_dispatcher::MidiDispatcher _midi_dispatcher{_engine.event_dispatcher()};\n    AudioFrontendMockup  _audio_frontend;\n    jsonconfig::JsonConfigurator _configurator{&_engine,\n                                               &_midi_dispatcher,\n                                               _engine.processor_container(),\n                                               _json_data};\n\n    std::unique_ptr<control::SushiControl> _module_under_test;\n};\n\nTEST_F(ControllerTest, TestMainEngineControls)\n{\n    auto transport_controller = _module_under_test->transport_controller();\n    ASSERT_TRUE(transport_controller);\n    EXPECT_FLOAT_EQ(TEST_SAMPLE_RATE, transport_controller->get_samplerate());\n    EXPECT_EQ(control::PlayingMode::PLAYING, transport_controller->get_playing_mode());\n    EXPECT_EQ(control::SyncMode::INTERNAL, transport_controller->get_sync_mode());\n    EXPECT_FLOAT_EQ(100.0f, transport_controller->get_tempo());\n    auto sig =  transport_controller->get_time_signature();\n    EXPECT_EQ(4, sig.numerator);\n    EXPECT_EQ(4, sig.denominator);\n\n    auto graph_controller = _module_under_test->audio_graph_controller();\n    ASSERT_TRUE(graph_controller);\n    auto tracks = graph_controller->get_all_tracks();\n\n    ASSERT_EQ(5u, tracks.size());\n    EXPECT_EQ(\"main\", tracks[0].name);\n    EXPECT_EQ(\"\", tracks[0].label);\n    EXPECT_EQ(2, tracks[0].channels);\n    EXPECT_EQ(1, tracks[0].buses);\n    EXPECT_EQ(control::TrackType::REGULAR, tracks[0].type);\n    EXPECT_EQ(3u, tracks[0].processors.size());\n\n    EXPECT_EQ(\"monotrack\", tracks[1].name);\n    EXPECT_EQ(\"\", tracks[1].label);\n    EXPECT_EQ(1, tracks[1].channels);\n    EXPECT_EQ(1, tracks[1].buses);\n    EXPECT_EQ(control::TrackType::REGULAR, tracks[1].type);\n    EXPECT_EQ(3u, tracks[1].processors.size());\n\n    EXPECT_EQ(\"monobustrack\", tracks[2].name);\n    EXPECT_EQ(\"\", tracks[2].label);\n    EXPECT_EQ(1, tracks[2].channels);\n    EXPECT_EQ(1, tracks[2].buses);\n    EXPECT_EQ(control::TrackType::REGULAR, tracks[2].type);\n    EXPECT_EQ(0u, tracks[2].processors.size());\n\n    EXPECT_EQ(\"multi\", tracks[3].name);\n    EXPECT_EQ(\"\", tracks[3].label);\n    EXPECT_EQ(4, tracks[3].channels);\n    EXPECT_EQ(2, tracks[3].buses);\n    EXPECT_EQ(control::TrackType::REGULAR, tracks[3].type);\n    EXPECT_EQ(0u, tracks[3].processors.size());\n\n    EXPECT_EQ(\"master\", tracks[4].name);\n    EXPECT_EQ(\"\", tracks[4].label);\n    EXPECT_EQ(ENGINE_CHANNELS, tracks[4].channels);\n    EXPECT_EQ(1, tracks[4].buses);\n    EXPECT_EQ(control::TrackType::POST, tracks[4].type);\n    EXPECT_EQ(0u, tracks[4].processors.size());\n}\n\nTEST_F(ControllerTest, TestKeyboardControls)\n{\n    auto keyboard_controller = _module_under_test->keyboard_controller();\n    ASSERT_TRUE(keyboard_controller);\n    /* No sanity checks on track ids is currently done, so these are just called to excercise the code */\n    EXPECT_EQ(control::ControlStatus::OK, keyboard_controller->send_note_on(0, 40, 0, 1.0));\n    EXPECT_EQ(control::ControlStatus::OK, keyboard_controller->send_note_off(0, 40, 0, 1.0));\n    EXPECT_EQ(control::ControlStatus::OK, keyboard_controller->send_note_aftertouch(0, 40, 0, 1.0));\n    EXPECT_EQ(control::ControlStatus::OK, keyboard_controller->send_pitch_bend(0, 0, 1.0));\n    EXPECT_EQ(control::ControlStatus::OK, keyboard_controller->send_aftertouch(0, 0, 1.0));\n    EXPECT_EQ(control::ControlStatus::OK, keyboard_controller->send_modulation(0, 0, 1.0));\n}\n\nTEST_F(ControllerTest, TestTrackControls)\n{\n    auto graph_controller = _module_under_test->audio_graph_controller();\n    ASSERT_TRUE(graph_controller);\n\n    auto [not_found_status, id_unused] = graph_controller->get_track_id(\"not_found\");\n    EXPECT_EQ(control::ControlStatus::NOT_FOUND, not_found_status);\n    auto [status, id] = graph_controller->get_track_id(\"main\");\n    ASSERT_EQ(control::ControlStatus::OK, status);\n\n    auto [track_not_found_status, processor_list] = graph_controller->get_track_processors(ObjectId(1234));\n    ASSERT_EQ(control::ControlStatus::NOT_FOUND, track_not_found_status);\n\n    auto [info_status, info] = graph_controller->get_track_info(id);\n    ASSERT_EQ(control::ControlStatus::OK, info_status);\n\n    EXPECT_EQ(\"main\", info.name);\n    EXPECT_EQ(\"\", info.label);\n    EXPECT_EQ(2, info.channels);\n    EXPECT_EQ(1, info.buses);\n    EXPECT_EQ(3u, info.processors.size());\n\n    auto [proc_status, processors] = graph_controller->get_track_processors(id);\n    ASSERT_EQ(control::ControlStatus::OK, proc_status);\n\n    EXPECT_EQ(3u, processors.size());\n    EXPECT_EQ(\"passthrough_0_l\", processors[0].name);\n    EXPECT_EQ(\"Passthrough\", processors[0].label);\n    EXPECT_EQ(0, processors[0].program_count);\n    EXPECT_EQ(0, processors[0].parameter_count);\n    EXPECT_EQ(info.processors[0], processors[0].id);\n\n    EXPECT_EQ(\"gain_0_l\", processors[1].name);\n    EXPECT_EQ(\"Gain\", processors[1].label);\n    EXPECT_EQ(0, processors[1].program_count);\n    EXPECT_EQ(1, processors[1].parameter_count);\n    EXPECT_EQ(info.processors[1], processors[1].id);\n\n    EXPECT_EQ(\"equalizer_0_l\", processors[2].name);\n    EXPECT_EQ(\"Equalizer\", processors[2].label);\n    EXPECT_EQ(0, processors[2].program_count);\n    EXPECT_EQ(3, processors[2].parameter_count);\n    EXPECT_EQ(info.processors[2], processors[2].id);\n\n    auto parameter_controller = _module_under_test->parameter_controller();\n    ASSERT_TRUE(parameter_controller);\n    auto [param_status, parameters] = parameter_controller->get_track_parameters(id);\n    ASSERT_EQ(control::ControlStatus::OK, param_status);\n\n    EXPECT_EQ(3u, parameters.size());\n    EXPECT_EQ(\"gain\", parameters[0].name);\n    EXPECT_EQ(\"Gain\", parameters[0].label);\n    EXPECT_EQ(\"dB\", parameters[0].unit);\n    EXPECT_EQ(control::ParameterType::FLOAT, parameters[0].type);\n    EXPECT_TRUE(parameters[0].automatable);\n    EXPECT_FLOAT_EQ(-120.0f, parameters[0].min_domain_value);\n    EXPECT_FLOAT_EQ(24.0f, parameters[0].max_domain_value);\n\n    EXPECT_EQ(\"pan\", parameters[1].name);\n    EXPECT_EQ(\"Pan\", parameters[1].label);\n    EXPECT_EQ(\"\", parameters[1].unit);\n    EXPECT_EQ(control::ParameterType::FLOAT, parameters[1].type);\n    EXPECT_TRUE(parameters[1].automatable);\n    EXPECT_FLOAT_EQ(-1.0f, parameters[1].min_domain_value);\n    EXPECT_FLOAT_EQ(1.0f, parameters[1].max_domain_value);\n\n    DECLARE_UNUSED(id_unused);\n}\n\nTEST_F(ControllerTest, TestProcessorControls)\n{\n    auto graph_controller = _module_under_test->audio_graph_controller();\n    ASSERT_TRUE(graph_controller);\n\n    auto [not_found_status, id_unused] = graph_controller->get_processor_id(\"not_found\");\n    EXPECT_EQ(control::ControlStatus::NOT_FOUND, not_found_status);\n    auto [status, id] = graph_controller->get_processor_id(\"equalizer_0_l\");\n    ASSERT_EQ(control::ControlStatus::OK, status);\n\n    auto [info_status, info] = graph_controller->get_processor_info(id);\n    ASSERT_EQ(control::ControlStatus::OK, info_status);\n\n    EXPECT_EQ(info.name, \"equalizer_0_l\");\n    EXPECT_EQ(info.label, \"Equalizer\");\n    EXPECT_EQ(info.program_count, 0);\n    EXPECT_EQ(info.parameter_count, 3);\n\n    auto [bypass_status, bypassed] = graph_controller->get_processor_bypass_state(id);\n    ASSERT_EQ(control::ControlStatus::OK, bypass_status);\n    ASSERT_FALSE(bypassed);\n\n    auto program_controller = _module_under_test->program_controller();\n    ASSERT_TRUE(program_controller);\n    auto [programs_status, prog_unused] = program_controller->get_processor_current_program(id);\n    ASSERT_EQ(control::ControlStatus::UNSUPPORTED_OPERATION, programs_status);\n\n    auto parameter_controller = _module_under_test->parameter_controller();\n    ASSERT_TRUE(parameter_controller);\n    auto [param_status, parameters] = parameter_controller->get_processor_parameters(id);\n    ASSERT_EQ(control::ControlStatus::OK, param_status);\n\n    EXPECT_EQ(3u, parameters.size());\n    EXPECT_EQ(\"frequency\", parameters[0].name);\n    EXPECT_EQ(\"Frequency\", parameters[0].label);\n    EXPECT_EQ(\"Hz\", parameters[0].unit);\n    EXPECT_EQ(control::ParameterType::FLOAT, parameters[0].type);\n    EXPECT_TRUE(parameters[0].automatable);\n    EXPECT_FLOAT_EQ(20.0f, parameters[0].min_domain_value);\n    EXPECT_FLOAT_EQ(20000.0f, parameters[0].max_domain_value);\n\n    EXPECT_EQ(\"gain\", parameters[1].name);\n    EXPECT_EQ(\"Gain\", parameters[1].label);\n    EXPECT_EQ(\"dB\", parameters[1].unit);\n    EXPECT_EQ(control::ParameterType::FLOAT, parameters[1].type);\n    EXPECT_TRUE(parameters[1].automatable);\n    EXPECT_FLOAT_EQ(-24.0f, parameters[1].min_domain_value);\n    EXPECT_FLOAT_EQ(24.0f, parameters[1].max_domain_value);\n\n    EXPECT_EQ(\"q\", parameters[2].name);\n    EXPECT_EQ(\"Q\", parameters[2].label);\n    EXPECT_EQ(\"\", parameters[2].unit);\n    EXPECT_EQ(control::ParameterType::FLOAT, parameters[2].type);\n    EXPECT_TRUE(parameters[2].automatable);\n    EXPECT_FLOAT_EQ(0.0f, parameters[2].min_domain_value);\n    EXPECT_FLOAT_EQ(10.0f, parameters[2].max_domain_value);\n\n    DECLARE_UNUSED(id_unused);\n    DECLARE_UNUSED(prog_unused);\n}\n\nTEST_F(ControllerTest, TestParameterControls)\n{\n    auto parameter_controller = _module_under_test->parameter_controller();\n    ASSERT_TRUE(parameter_controller);\n    auto graph_controller = _module_under_test->audio_graph_controller();\n    ASSERT_TRUE(graph_controller);\n\n    auto [status, proc_id] = graph_controller->get_processor_id(\"equalizer_0_l\");\n    ASSERT_EQ(control::ControlStatus::OK, status);\n    auto [found_status, id] = parameter_controller->get_parameter_id(proc_id, \"frequency\");\n    ASSERT_EQ(control::ControlStatus::OK, found_status);\n\n    auto [info_status, info] = parameter_controller->get_parameter_info(proc_id, id);\n    ASSERT_EQ(control::ControlStatus::OK, info_status);\n\n    EXPECT_EQ(\"frequency\", info.name);\n    EXPECT_EQ(\"Frequency\", info.label);\n    EXPECT_EQ(\"Hz\", info.unit);\n    EXPECT_EQ(control::ParameterType::FLOAT, info.type);\n    EXPECT_EQ(true, info.automatable);\n    EXPECT_FLOAT_EQ(20.0f, info.min_domain_value);\n    EXPECT_FLOAT_EQ(20000.0f, info.max_domain_value);\n\n    auto [value_status, value] = parameter_controller->get_parameter_value_in_domain(proc_id, id);\n    ASSERT_EQ(control::ControlStatus::OK, value_status);\n    EXPECT_FLOAT_EQ(1000.0f, value);\n\n    auto [norm_value_status,norm_value] = parameter_controller->get_parameter_value(proc_id, id);\n    ASSERT_EQ(control::ControlStatus::OK, norm_value_status);\n    EXPECT_GE(norm_value, 0.0f);\n    EXPECT_LE(norm_value, 1.0f);\n\n    auto [str_value_status, str_value] = parameter_controller->get_parameter_value_as_string(proc_id, id);\n    ASSERT_EQ(control::ControlStatus::OK, str_value_status);\n    EXPECT_EQ(\"1000.00\", str_value);\n}\n"
  },
  {
    "path": "test/unittests/engine/controllers/audio_graph_controller_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"engine/audio_engine.h\"\n#include \"control_frontends/base_control_frontend.h\"\n#include \"test_utils/engine_mockup.h\"\n#include \"engine/controller/audio_graph_controller.cpp\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\nusing namespace sushi::internal::engine::controller_impl;\n\nconstexpr float TEST_SAMPLE_RATE = 44100;\n\nclass AudioGraphControllerTest : public ::testing::Test\n{\nprotected:\n    AudioGraphControllerTest() = default;\n\n    void SetUp() override\n    {\n        bool debug_mode_sw = false;\n        _audio_engine = std::make_unique<AudioEngine>(TEST_SAMPLE_RATE, 1, \"\", debug_mode_sw, new EventDispatcherMockup());\n        _event_dispatcher_mockup = static_cast<EventDispatcherMockup*>(_audio_engine->event_dispatcher());\n        _module_under_test = std::make_unique<AudioGraphController>(_audio_engine.get(), _event_dispatcher_mockup);\n\n        _audio_engine->set_audio_channels(8, 8);\n\n        _audio_engine->create_track(\"Track 1\", 2, std::optional<int>());\n        _track_id = _audio_engine->processor_container()->track(\"Track 1\")->id();\n    }\n    \n    EventDispatcherMockup*                _event_dispatcher_mockup{nullptr};\n    std::unique_ptr<AudioEngine>          _audio_engine;\n    std::unique_ptr<AudioGraphController> _module_under_test;\n    int _track_id;\n};\n\nTEST_F(AudioGraphControllerTest, TestGettingProcessors)\n{\n    auto processors = _module_under_test->get_all_processors();\n    ASSERT_EQ(1u, processors.size());\n    EXPECT_EQ(_track_id, processors[0].id);\n\n    auto tracks = _module_under_test->get_all_tracks();\n    ASSERT_EQ(1u, tracks.size());\n    EXPECT_EQ(_track_id, tracks[0].id);\n\n    auto [track_status, track] = _module_under_test->get_track_info(_track_id);\n    ASSERT_EQ(control::ControlStatus::OK, track_status);\n    EXPECT_EQ(_track_id, track.id);\n    EXPECT_EQ(2, track.channels);\n    EXPECT_EQ(1, track.buses);\n    EXPECT_EQ(\"Track 1\", track.name);\n\n    auto [proc_status, track_proc] = _module_under_test->get_track_processors(_track_id);\n    ASSERT_EQ(control::ControlStatus::OK, proc_status);\n    EXPECT_EQ(0u, track_proc.size());\n\n    auto [status, id] = _module_under_test->get_processor_id(\"Track 1\");\n    ASSERT_EQ(control::ControlStatus::OK, status);\n    EXPECT_EQ(_track_id, id);\n\n    auto [proc_status_2, proc] = _module_under_test->get_processor_info(_track_id);\n    ASSERT_EQ(control::ControlStatus::OK, proc_status_2);\n    EXPECT_EQ(_track_id, proc.id);\n    EXPECT_EQ(0, proc.program_count);\n    EXPECT_EQ(3, proc.parameter_count);\n    EXPECT_EQ(\"Track 1\", proc.name);\n\n    std::tie(status, id) = _module_under_test->get_track_id(\"Track 1\");\n    ASSERT_EQ(control::ControlStatus::OK, status);\n    EXPECT_EQ(_track_id, id);\n\n    std::tie(status, id) = _module_under_test->get_processor_id(\"Track 2\");\n    ASSERT_EQ(control::ControlStatus::NOT_FOUND, status);\n\n    std::tie(status, id) = _module_under_test->get_track_id(\"Track 2\");\n    ASSERT_EQ(control::ControlStatus::NOT_FOUND, status);\n\n    auto [bypass_status, bypassed] = _module_under_test->get_processor_bypass_state(_track_id);\n    ASSERT_EQ(control::ControlStatus::OK, bypass_status);\n    EXPECT_FALSE(bypassed);\n}\n\nTEST_F(AudioGraphControllerTest, TestCreatingAndRemovingTracks)\n{\n    auto status = _module_under_test->create_track(\"Track 2\", 2, std::optional<int>());\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, status.status);\n\n    auto execution_status1 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status1, EventStatus::HANDLED_OK);\n\n    auto tracks = _audio_engine->processor_container()->all_tracks();\n    ASSERT_EQ(2u, tracks.size());\n    EXPECT_EQ(\"Track 2\", tracks[1]->name());\n    EXPECT_EQ(2, tracks[1]->input_channels());\n    EXPECT_EQ(2, tracks[1]->output_channels());\n\n    status = _module_under_test->create_multibus_track(\"Track 3\", 2, std::optional<int>());\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, status.status);\n\n    auto execution_status2 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status2, EventStatus::HANDLED_OK);\n\n    status = _module_under_test->create_pre_track(\"Track 4\");\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, status.status);\n\n    auto execution_status_3 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status_3, EventStatus::HANDLED_OK);\n\n    status = _module_under_test->create_post_track(\"Track 5\");\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, status.status);\n\n    auto execution_status_4 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status_4, EventStatus::HANDLED_OK);\n\n    tracks = _audio_engine->processor_container()->all_tracks();\n    ASSERT_EQ(5u, tracks.size());\n    EXPECT_EQ(\"Track 3\", tracks[2]->name());\n    EXPECT_EQ(2, tracks[2]->buses());\n\n    status = _module_under_test->delete_track(tracks[2]->id());\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, status.status);\n\n    auto execution_status_5 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status_5, EventStatus::HANDLED_OK);\n\n    tracks = _audio_engine->processor_container()->all_tracks();\n    ASSERT_EQ(4u, tracks.size());\n}\n\nTEST_F(AudioGraphControllerTest, TestCreatingAndRemovingProcessors)\n{\n    auto status = _module_under_test->create_processor_on_track(\"Proc 1\",\n                                                                \"sushi.testing.gain\",\n                                                                \"\",\n                                                                control::PluginType::INTERNAL,\n                                                                _track_id,\n                                                                std::nullopt);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, status.status);\n\n    auto execution_status1 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status1, EventStatus::HANDLED_OK);\n\n    auto processors = _audio_engine->processor_container()->processors_on_track(_track_id);\n    ASSERT_EQ(1u, processors.size());\n    EXPECT_EQ(\"Proc 1\", processors[0]->name());\n    int proc_id = processors[0]->id();\n\n    // Create a new track and move the processor there\n    auto [track_status, track_2_id] = _audio_engine->create_track(\"Track 2\", 2, std::optional<int>());\n    ASSERT_EQ(EngineReturnStatus::OK, track_status);\n\n    status = _module_under_test->move_processor_on_track(proc_id, _track_id, track_2_id, std::nullopt);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, status.status);\n\n    auto execution_status2 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status2, EventStatus::HANDLED_OK);\n\n    EXPECT_EQ(0u, _audio_engine->processor_container()->processors_on_track(_track_id).size());\n    EXPECT_EQ(1u, _audio_engine->processor_container()->processors_on_track(track_2_id).size());\n\n    // Delete the processor from the new track\n    status = _module_under_test->delete_processor_from_track(proc_id, track_2_id);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, status.status);\n\n    auto execution_status4 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status4, EventStatus::HANDLED_OK);\n\n    processors = _audio_engine->processor_container()->processors_on_track(track_2_id);\n    EXPECT_EQ(0u, processors.size());\n}\n"
  },
  {
    "path": "test/unittests/engine/controllers/audio_routing_controller_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"engine/audio_engine.h\"\n#include \"control_frontends/base_control_frontend.h\"\n#include \"test_utils/engine_mockup.h\"\n#include \"engine/controller/audio_routing_controller.cpp\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\nusing namespace sushi::internal::engine::controller_impl;\n\nconstexpr float TEST_SAMPLE_RATE = 44100;\n\nclass AudioRoutingControllerTest : public ::testing::Test\n{\nprotected:\n    AudioRoutingControllerTest() = default;\n\n    void SetUp() override\n    {\n        bool debug_mode_sw = false;\n        _audio_engine = std::make_unique<AudioEngine>(TEST_SAMPLE_RATE, 1, \"\", debug_mode_sw, new EventDispatcherMockup());\n        _event_dispatcher_mockup = static_cast<EventDispatcherMockup*>(_audio_engine->event_dispatcher());\n        _module_under_test = std::make_unique<AudioRoutingController>(_audio_engine.get(), _event_dispatcher_mockup);\n\n        _audio_engine->set_audio_channels(8, 8);\n\n        _audio_engine->create_track(\"Track 1\", 2, std::optional<int>());\n        _track_id = _audio_engine->processor_container()->track(\"Track 1\")->id();\n    }\n\n    EventDispatcherMockup*                  _event_dispatcher_mockup{nullptr};\n    std::unique_ptr<AudioEngine>            _audio_engine;\n    std::unique_ptr<AudioRoutingController> _module_under_test;\n    ObjectId _track_id;\n};\n\nTEST_F(AudioRoutingControllerTest, TestGettingAudioRouting)\n{\n    auto connections = _module_under_test->get_all_input_connections();\n    EXPECT_EQ(0u, connections.size());\n    connections = _module_under_test->get_all_output_connections();\n    EXPECT_EQ(0u, connections.size());\n    auto connection_response = _module_under_test->get_input_connections_for_track(static_cast<int>(_track_id));\n    EXPECT_EQ(0u, connection_response.second.size());\n    EXPECT_EQ(control::ControlStatus::OK, connection_response.first);\n    connection_response = _module_under_test->get_output_connections_for_track(static_cast<int>(_track_id));\n    EXPECT_EQ(0u, connection_response.second.size());\n    EXPECT_EQ(control::ControlStatus::OK, connection_response.first);\n\n    // Connect the track to input channels 2 & 3 and output channels 4 & 5\n    auto status = _audio_engine->connect_audio_input_bus(1, 0, _track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    status = _audio_engine->connect_audio_output_bus(2, 0, _track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    connections = _module_under_test->get_all_input_connections();\n    ASSERT_EQ(2u, connections.size());\n    EXPECT_EQ(2, connections[0].engine_channel);\n    EXPECT_EQ(0, connections[0].track_channel);\n    EXPECT_EQ(_track_id, ObjectId(connections[0].track_id));\n    EXPECT_EQ(3, connections[1].engine_channel);\n    EXPECT_EQ(1, connections[1].track_channel);\n    EXPECT_EQ(_track_id, ObjectId(connections[1].track_id));\n\n    connections = _module_under_test->get_all_output_connections();\n    ASSERT_EQ(2u, connections.size());\n    EXPECT_EQ(4, connections[0].engine_channel);\n    EXPECT_EQ(0, connections[0].track_channel);\n    EXPECT_EQ(_track_id, ObjectId(connections[0].track_id));\n    EXPECT_EQ(5, connections[1].engine_channel);\n    EXPECT_EQ(1, connections[1].track_channel);\n    EXPECT_EQ(_track_id, ObjectId(connections[1].track_id));\n\n    connection_response = _module_under_test->get_input_connections_for_track(static_cast<int>(_track_id));\n    EXPECT_EQ(control::ControlStatus::OK, connection_response.first);\n    EXPECT_EQ(2u, connection_response.second.size());\n    connection_response = _module_under_test->get_output_connections_for_track(static_cast<int>(_track_id));\n    EXPECT_EQ(control::ControlStatus::OK, connection_response.first);\n    EXPECT_EQ(2u, connection_response.second.size());\n\n    // Test for non-existing tracks as well\n    connection_response = _module_under_test->get_input_connections_for_track(12345);\n    EXPECT_EQ(control::ControlStatus::NOT_FOUND, connection_response.first);\n    connection_response = _module_under_test->get_output_connections_for_track(23456);\n    EXPECT_EQ(control::ControlStatus::NOT_FOUND, connection_response.first);\n}\n\nTEST_F(AudioRoutingControllerTest, TestSettingAudioRouting)\n{\n    // Connect using controller functions (using events)\n    auto response = _module_under_test->connect_input_channel_to_track(_track_id, 0, 2);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, response.status);\n\n    auto execution_status1 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status1, EventStatus::HANDLED_OK);\n\n    response = _module_under_test->connect_input_channel_to_track(_track_id, 1, 3);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, response.status);\n\n    auto execution_status2 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status2, EventStatus::HANDLED_OK);\n\n    auto connections = _module_under_test->get_all_input_connections();\n    ASSERT_EQ(2u, connections.size());\n    EXPECT_EQ(2, connections[0].engine_channel);\n    EXPECT_EQ(0, connections[0].track_channel);\n    EXPECT_EQ(_track_id, ObjectId(connections[0].track_id));\n    EXPECT_EQ(3, connections[1].engine_channel);\n    EXPECT_EQ(1, connections[1].track_channel);\n    EXPECT_EQ(_track_id, ObjectId(connections[1].track_id));\n\n    // Do the same for output connections\n    response = _module_under_test->connect_output_channel_to_track(_track_id, 0, 4);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, response.status);\n\n    auto execution_status3 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status3, EventStatus::HANDLED_OK);\n\n    response = _module_under_test->connect_output_channel_to_track(_track_id, 1, 5);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, response.status);\n\n    auto execution_status4 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status4, EventStatus::HANDLED_OK);\n\n    connections = _module_under_test->get_all_output_connections();\n    ASSERT_EQ(2u, connections.size());\n    EXPECT_EQ(4, connections[0].engine_channel);\n    EXPECT_EQ(0, connections[0].track_channel);\n    EXPECT_EQ(_track_id, ObjectId(connections[0].track_id));\n    EXPECT_EQ(5, connections[1].engine_channel);\n    EXPECT_EQ(1, connections[1].track_channel);\n    EXPECT_EQ(_track_id, ObjectId(connections[1].track_id));\n}\n\nTEST_F(AudioRoutingControllerTest, TestRemovingAudioRouting)\n{\n    // Connect the track to input channels 2 & 3 and output channels 4 & 5\n    auto engine_status = _audio_engine->connect_audio_input_bus(1, 0, _track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, engine_status);\n    engine_status = _audio_engine->connect_audio_output_bus(2, 0, _track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, engine_status);\n\n    // Disconnect using controller functions (using events)\n    auto response = _module_under_test->disconnect_input(_track_id, 0, 2);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, response.status);\n\n    auto execution_status_1 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status_1, EventStatus::HANDLED_OK);\n\n    auto connections = _module_under_test->get_all_input_connections();\n    EXPECT_EQ(1u, connections.size());\n    connections = _module_under_test->get_all_output_connections();\n    EXPECT_EQ(2u, connections.size());\n\n    response = _module_under_test->disconnect_all_outputs_from_track(_track_id);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, response.status);\n\n    auto execution_status_2 = _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n    ASSERT_EQ(execution_status_2, EventStatus::HANDLED_OK);\n\n    connections = _module_under_test->get_all_input_connections();\n    EXPECT_EQ(1u, connections.size());\n    connections = _module_under_test->get_all_output_connections();\n    EXPECT_EQ(0u, connections.size());\n}\n"
  },
  {
    "path": "test/unittests/engine/controllers/midi_controller_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"engine/audio_engine.h\"\n#include \"library/midi_encoder.h\"\n\n#include \"control_frontends/base_control_frontend.h\"\n#include \"test_utils/engine_mockup.h\"\n#include \"test_utils/control_mockup.h\"\n#include \"test_utils/mock_midi_frontend.h\"\n#include \"engine/controller/midi_controller.cpp\"\n\nusing ::testing::NiceMock;\nusing ::testing::_;\n\nusing namespace midi;\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\nusing namespace sushi::internal::control_frontend;\nusing namespace sushi::internal::midi_dispatcher;\nusing namespace controller_impl;\n\nconstexpr float TEST_SAMPLE_RATE = 44100;\n\nconst MidiDataByte TEST_NOTE_OFF_CH3 = {0x82, 60, 45, 0}; /* Channel 3 */\nconst MidiDataByte TEST_CTRL_CH_CH4_67 = {0xB3, 67, 75, 0}; /* Channel 4, cc 67 */\nconst MidiDataByte TEST_CTRL_CH_CH4_68 = {0xB3, 68, 75, 0}; /* Channel 4, cc 68 */\nconst MidiDataByte TEST_CTRL_CH_CH4_70 = {0xB3, 70, 75, 0}; /* Channel 4, cc 70 */\nconst MidiDataByte TEST_PRG_CH_CH5 = {0xC4, 40, 0, 0};  /* Channel 5, prg 40 */\nconst MidiDataByte TEST_PRG_CH_CH6 = {0xC5, 40, 0, 0};  /* Channel 6, prg 40 */\nconst MidiDataByte TEST_PRG_CH_CH7 = {0xC6, 40, 0, 0};  /* Channel 7, prg 40 */\n\nclass MidiControllerEventTestFrontend : public ::testing::Test\n{\nprotected:\n    MidiControllerEventTestFrontend() = default;\n\n    void SetUp() override\n    {\n        _test_dispatcher = static_cast<EventDispatcherMockup*>(_test_engine.event_dispatcher());\n        _midi_dispatcher.set_frontend(&_mock_frontend);\n        _midi_controller = std::make_unique<MidiController>(&_test_engine, &_midi_dispatcher, _test_dispatcher);\n    }\n\n    EngineMockup _test_engine{TEST_SAMPLE_RATE};\n    MidiDispatcher _midi_dispatcher{_test_engine.event_dispatcher()};\n    sushi::control::ControlMockup _controller; // TODO: Maybe just the ParameterControllerMockup?\n    EventDispatcherMockup* _test_dispatcher;\n    std::unique_ptr<MidiController> _midi_controller;\n    ::testing::NiceMock<MockMidiFrontend> _mock_frontend{nullptr};\n};\n\nTEST_F(MidiControllerEventTestFrontend, TestKbdInputConectionDisconnection)\n{\n    auto track = _test_engine.processor_container()->track(\"track 1\");\n    ObjectId track_id = track->id();\n    bool raw_midi = false;\n    control::MidiChannel channel = sushi::control::MidiChannel::MIDI_CH_3;\n    int port = 1;\n\n    _midi_dispatcher.set_midi_inputs(5);\n\n    _midi_dispatcher.send_midi(port, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    auto event_response_connect = _midi_controller->connect_kbd_input_to_track(track_id, channel, port, raw_midi);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_connect.status);\n    // That the engine is passed as argument to execute violates the Liskov Substitution Principle and should not be necessary.\n    // A refactor to how events work would solve that.\n\n    auto execution_status_1 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status_1, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    auto event_response_disconnect =  _midi_controller->disconnect_kbd_input(track_id, channel, port, raw_midi);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_disconnect.status);\n    auto execution_status_2 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status_2, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n}\n\nTEST_F(MidiControllerEventTestFrontend, TestKbdInputConectionDisconnectionRaw)\n{\n    auto track = _test_engine.processor_container()->track(\"track 1\");\n    ObjectId track_id = track->id();\n    bool raw_midi = true;\n    control::MidiChannel channel = sushi::control::MidiChannel::MIDI_CH_3;\n    int port = 1;\n\n    _midi_dispatcher.set_midi_inputs(5);\n\n    auto event_response_connect = _midi_controller->connect_kbd_input_to_track(track_id, channel, port, raw_midi);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_connect.status);\n    auto execution_status1 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status1, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    auto event_response_disconnect =  _midi_controller->disconnect_kbd_input(track_id, channel, port, raw_midi);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_disconnect.status);\n    auto execution_status2 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status2, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n}\n\nTEST_F(MidiControllerEventTestFrontend, TestKbdOutputConectionDisconnection)\n{\n    auto track = _test_engine.processor_container()->track(\"track 1\");\n    ObjectId track_id = track->id();\n    int port = 0;\n\n    _midi_dispatcher.set_midi_outputs(5);\n\n    control::MidiChannel channel_3 = sushi::control::MidiChannel::MIDI_CH_3;\n\n    int int_channel_3 = int_from_ext_midi_channel(channel_3);\n\n    KeyboardEvent event_ch3(KeyboardEvent::Subtype::NOTE_ON,\n                            track_id,\n                            int_channel_3,\n                            48,\n                            0.5f,\n                            IMMEDIATE_PROCESS);\n\n    /* Send midi message without connections */\n    auto event = std::make_unique<KeyboardEvent>(event_ch3);\n    auto status_1 = _midi_dispatcher.process(event.get());\n    EXPECT_EQ(EventStatus::HANDLED_OK, status_1);\n\n    auto event_response_connect = _midi_controller->connect_kbd_output_from_track(track_id, channel_3, port);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_connect.status);\n    auto execution_status_1 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status_1, EventStatus::HANDLED_OK);\n\n    EXPECT_CALL(_mock_frontend, send_midi(0, midi::encode_note_on(2, 48, 0.5f), _)).Times(1);\n    event = std::make_unique<KeyboardEvent>(event_ch3);\n    auto status_2 = _midi_dispatcher.process(event.get());\n    EXPECT_EQ(EventStatus::HANDLED_OK, status_2);\n\n    auto event_response_disconnect =  _midi_controller->disconnect_kbd_output(track_id, channel_3, port);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_disconnect.status);\n    auto execution_status_2 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status_2, EventStatus::HANDLED_OK);\n\n    event = std::make_unique<KeyboardEvent>(event_ch3);\n    auto status3 = _midi_dispatcher.process(event.get());\n    EXPECT_EQ(EventStatus::HANDLED_OK, status3);\n}\n\nTEST_F(MidiControllerEventTestFrontend, TestCCDataConnectionDisconnection)\n{\n    control::MidiChannel channel = sushi::control::MidiChannel::MIDI_CH_4;\n    int port = 0;\n\n    // The id for the mock processor is generated by a static atomic counter in BaseIdGenetator, so needs to be fetched.\n    auto processor = _test_engine.processor_container()->processor(\"processor\");\n    ObjectId processor_id = processor->id();\n\n    const auto parameter = processor->parameter_from_name(\"param 1\");\n    ObjectId parameter_id = parameter->id();\n\n    _midi_dispatcher.set_midi_inputs(5);\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_67, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_68, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_70, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    // Connect CC Number 67:\n\n    auto event_response_connect_1 = _midi_controller->connect_cc_to_parameter(processor_id,\n                                                                              parameter_id,\n                                                                              channel,\n                                                                              port,\n                                                                              67, // cc_number\n                                                                              0, // min_range\n                                                                              100, // max_range\n                                                                              false); // use_relative_mode\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_connect_1.status);\n    auto execution_status1 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status1, EventStatus::HANDLED_OK);\n\n    // Connect CC Number 68:\n\n    auto event_response_connect_2 = _midi_controller->connect_cc_to_parameter(processor_id,\n                                                                              parameter_id,\n                                                                              channel,\n                                                                              port,\n                                                                              68, // cc_number\n                                                                              0, // min_range\n                                                                              100, // max_range\n                                                                              false); // use_relative_mode\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_connect_2.status);\n    auto execution_status2 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status2, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_67, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_68, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_70, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    // Connect CC Number 70:\n\n    auto event_response_connect_3 = _midi_controller->connect_cc_to_parameter(processor_id,\n                                                                              parameter_id,\n                                                                              channel,\n                                                                              port,\n                                                                              70, // cc_number\n                                                                              0, // min_range\n                                                                              100, // max_range\n                                                                              false); // use_relative_mode\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_connect_3.status);\n    auto execution_status3 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status3, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_67, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_68, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_70, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    // Disconnect CC Number 67 only:\n\n    auto event_response_disconnect = _midi_controller->disconnect_cc(processor_id,\n                                                                     channel,\n                                                                     port,\n                                                                     67); // cc_number\n\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_disconnect.status);\n    auto execution_status_disconnect = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status_disconnect, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_67, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_68, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_70, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    // Disconnect all remaining CC's:\n\n    auto event_response_disconnect_all = _midi_controller->disconnect_all_cc_from_processor(processor_id);\n\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_disconnect_all.status);\n    auto execution_status_disconnect_all = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status_disconnect_all, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_67, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_68, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_CTRL_CH_CH4_70, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n}\n\nTEST_F(MidiControllerEventTestFrontend, TestPCDataConnectionDisconnection)\n{\n    int port = 0;\n\n    // The id for the mock processor is generated by a static atomic counter in BaseIdGenetator, so needs to be fetched.\n    auto processor = _test_engine.processor_container()->processor(\"processor\");\n    ObjectId processor_id = processor->id();\n\n    _midi_dispatcher.set_midi_inputs(5);\n\n    // Connect Channel 5:\n\n    _midi_dispatcher.send_midi(port, TEST_PRG_CH_CH5, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    auto event_response_connect_1 = _midi_controller->connect_pc_to_processor(processor_id,\n                                                                              sushi::control::MidiChannel::MIDI_CH_5,\n                                                                              port);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_connect_1.status);\n    auto execution_status1 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status1, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_PRG_CH_CH5, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    // Connect Channel 6:\n\n    _midi_dispatcher.send_midi(port, TEST_PRG_CH_CH6, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    auto event_response_connect_2 = _midi_controller->connect_pc_to_processor(processor_id,\n                                                                              sushi::control::MidiChannel::MIDI_CH_6,\n                                                                              port);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_connect_2.status);\n    auto execution_status2 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status2, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_PRG_CH_CH6, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    // Connect Channel 7:\n\n    _midi_dispatcher.send_midi(port, TEST_PRG_CH_CH7, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    auto event_response_connect_3 = _midi_controller->connect_pc_to_processor(processor_id,\n                                                                              sushi::control::MidiChannel::MIDI_CH_7,\n                                                                              port);\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_connect_3.status);\n    auto execution_status3 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status3, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_PRG_CH_CH7, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    // Disconnect Channel 5 only:\n\n    auto event_response_disconnect_1 = _midi_controller->disconnect_pc(processor_id,\n                                                                       sushi::control::MidiChannel::MIDI_CH_5,\n                                                                       port);\n\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_disconnect_1.status);\n    auto execution_status4 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status4, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_PRG_CH_CH5, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_PRG_CH_CH6, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_PRG_CH_CH7, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher->got_event());\n\n    // Disconnect all channels:\n\n    auto event_response_disconnect_all = _midi_controller->disconnect_all_pc_from_processor(processor_id);\n\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_disconnect_all.status);\n    auto execution_status_disconnect_all = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status_disconnect_all, EventStatus::HANDLED_OK);\n\n    _midi_dispatcher.send_midi(port, TEST_PRG_CH_CH5, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_PRG_CH_CH6, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n\n    _midi_dispatcher.send_midi(port, TEST_PRG_CH_CH7, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher->got_event());\n}\n\nTEST_F(MidiControllerEventTestFrontend, TestSettingClockOutput)\n{\n    int port = 0;\n    _midi_dispatcher.set_midi_outputs(1);\n    EXPECT_EQ(control::ControlStatus::OK, _midi_controller->set_midi_clock_output_enabled(true, port));\n    EXPECT_EQ(EventStatus::HANDLED_OK, _test_dispatcher->execute_engine_event(&_test_engine));\n\n    EXPECT_EQ(control::ControlStatus::OK, _midi_controller->set_midi_clock_output_enabled(true, 1234));\n    EXPECT_NE(EventStatus::HANDLED_OK, _test_dispatcher->execute_engine_event(&_test_engine));\n\n    _midi_dispatcher.enable_midi_clock(true, port);\n    EXPECT_TRUE(_midi_controller->get_midi_clock_output_enabled(port));\n    EXPECT_FALSE(_midi_controller->get_midi_clock_output_enabled(1234));\n}"
  },
  {
    "path": "test/unittests/engine/controllers/osc_controller_test.cpp",
    "content": "#include <gmock/gmock.h>\n#include <gmock/gmock-actions.h>\n\n#include \"engine/audio_engine.h\"\n\n#include \"control_frontends/base_control_frontend.h\"\n#include \"test_utils/engine_mockup.h\"\n#include \"test_utils/control_mockup.h\"\n#include \"engine/controller/osc_controller.cpp\"\n\n#include \"test_utils/mock_osc_interface.h\"\n\nusing ::testing::Return;\nusing ::testing::NiceMock;\nusing ::testing::_;\n\nusing namespace midi;\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\nusing namespace sushi::internal::control_frontend;\nusing namespace sushi::internal::midi_dispatcher;\nusing namespace sushi::internal::engine::controller_impl;\n\nconstexpr float TEST_SAMPLE_RATE = 44100;\nconstexpr int OSC_TEST_SERVER_PORT = 24024;\nconstexpr int OSC_TEST_SEND_PORT = 24023;\nconstexpr auto OSC_TEST_SEND_ADDRESS = \"127.0.0.1\";\n\nclass OscControllerEventTestFrontend : public ::testing::Test\n{\nprotected:\n    OscControllerEventTestFrontend() = default;\n\n    void SetUp() override\n    {\n        auto mock_osc_interface = new NiceMock<MockOscInterface>(OSC_TEST_SERVER_PORT,\n                                                                 OSC_TEST_SEND_PORT,\n                                                                 OSC_TEST_SEND_ADDRESS);\n\n        _osc_frontend = std::make_unique<OSCFrontend>(&_test_engine, &_controller, mock_osc_interface);\n\n        _test_dispatcher = static_cast<EventDispatcherMockup*>(_test_engine.event_dispatcher());\n        _osc_controller = std::make_unique<OscController>(&_test_engine, _test_dispatcher);\n\n        EXPECT_CALL(*mock_osc_interface, init()).Times(1).WillOnce(Return(true));\n\n        ASSERT_EQ(ControlFrontendStatus::OK, _osc_frontend->init());\n        _osc_controller->set_osc_frontend(_osc_frontend.get());\n    }\n\n    EngineMockup _test_engine{TEST_SAMPLE_RATE};\n\n    sushi::control::ControlMockup _controller;\n    std::unique_ptr<OscController> _osc_controller;\n\n    std::unique_ptr<OSCFrontend> _osc_frontend;\n\n    EventDispatcherMockup* _test_dispatcher;\n};\n\nTEST_F(OscControllerEventTestFrontend, TestBasicPolling)\n{\n    auto send_port = _osc_controller->get_send_port();\n    auto receive_port = _osc_controller->get_receive_port();\n\n    ASSERT_EQ(send_port, 24023);\n    ASSERT_EQ(receive_port, 24024);\n\n    auto enabled_outputs = _osc_controller->get_enabled_parameter_outputs();\n\n    ASSERT_EQ(enabled_outputs.size(), 0u);\n}\n\nTEST_F(OscControllerEventTestFrontend, TestEnablingAndDisablingOfOSCOutput)\n{\n    // The id for the mock processor is generated by a static atomic counter in BaseIdGenetator, so needs to be fetched.\n    auto processor = _test_engine.processor_container()->processor(\"processor\");\n    ObjectId processor_id = processor->id();\n\n    const auto parameter = processor->parameter_from_name(\"param 1\");\n    ObjectId parameter_id = parameter->id();\n\n    auto event_response_enable = _osc_controller->enable_output_for_parameter(processor_id, parameter_id);\n\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_enable.status);\n    auto execution_status_1 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status_1, EventStatus::HANDLED_OK);\n\n    auto enabled_outputs1 = _osc_controller->get_enabled_parameter_outputs();\n\n    ASSERT_EQ(enabled_outputs1.size(), 1u);\n\n    ASSERT_EQ(enabled_outputs1[0], \"/parameter/processor/param_1\");\n\n    auto event_response_disable = _osc_controller->disable_output_for_parameter(processor_id, parameter_id);\n\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, event_response_disable.status);\n    auto execution_status_2 = _test_dispatcher->execute_engine_event(&_test_engine);\n    ASSERT_EQ(execution_status_2, EventStatus::HANDLED_OK);\n\n    auto enabled_outputs2 = _osc_controller->get_enabled_parameter_outputs();\n\n    ASSERT_EQ(enabled_outputs2.size(), 0u);\n}"
  },
  {
    "path": "test/unittests/engine/controllers/reactive_controller_test.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include <gmock/gmock.h>\n#include <gmock/gmock-actions.h>\n\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/event_dispatcher_accessor.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"engine/controller/real_time_controller.cpp\"\n\n#include \"audio_frontends/reactive_frontend.cpp\"\n#include \"control_frontends/reactive_midi_frontend.cpp\"\n\n#include \"test_utils/audio_frontend_mockup.h\"\n#include \"test_utils/engine_mockup.h\"\n#include \"test_utils/control_mockup.h\"\n\nnamespace sushi::internal {\n\nclass RtControllerAccessor\n{\npublic:\n    explicit RtControllerAccessor(RealTimeController& f) : _friend(f) {}\n\n    [[nodiscard]] float tempo() const\n    {\n        return _friend._tempo;\n    }\n\n    engine::Transport* transport()\n    {\n        return _friend._transport;\n    }\n\n    [[nodiscard]] sushi::TimeSignature time_signature() const\n    {\n        return _friend._time_signature;\n    }\n\n    [[nodiscard]] control::PlayingMode playing_mode() const\n    {\n        return _friend._playing_mode;\n    }\n\nprivate:\n    RealTimeController& _friend;\n};\n\n}\n\nusing namespace std::chrono_literals;\n\nusing ::testing::Return;\nusing ::testing::NiceMock;\nusing ::testing::_;\n\nusing namespace sushi;\nusing namespace sushi::internal;\n\nconstexpr float TEST_SAMPLE_RATE = 44100;\n\nclass ReactiveControllerTestFrontend : public ::testing::Test\n{\npublic:\n    void crank_event_loop_once()\n    {\n        _event_dispatcher_accessor->running() = false;\n        _event_dispatcher_accessor->event_loop();\n    }\n\nprotected:\n    ReactiveControllerTestFrontend() = default;\n\n    void SetUp() override\n    {\n        _mock_engine = std::make_unique<EngineMockup>(TEST_SAMPLE_RATE);\n\n        _event_dispatcher = std::make_unique<dispatcher::EventDispatcher>(_mock_engine.get(), &_in_rt_queue, &_out_rt_queue);\n\n        _event_dispatcher_accessor = std::make_unique<sushi::internal::dispatcher::Accessor>(*_event_dispatcher);\n\n        _audio_frontend = std::make_unique<ReactiveFrontend>(_mock_engine.get());\n\n        _midi_dispatcher = std::make_unique<midi_dispatcher::MidiDispatcher>(_event_dispatcher.get());\n\n        _midi_frontend = std::make_unique<midi_frontend::ReactiveMidiFrontend>(_midi_dispatcher.get());\n\n        _real_time_controller = std::make_unique<RealTimeController>(_audio_frontend.get(),\n                                                                     _midi_frontend.get(),\n                                                                     &_transport);\n\n        _accessor = std::make_unique<sushi::internal::RtControllerAccessor>(*_real_time_controller);\n    }\n\n    std::unique_ptr<RealTimeController>          _real_time_controller;\n    std::unique_ptr<dispatcher::EventDispatcher> _event_dispatcher;\n    std::unique_ptr<sushi::internal::dispatcher::Accessor> _event_dispatcher_accessor;\n    std::unique_ptr<EngineMockup>                _mock_engine;\n    std::unique_ptr<ReactiveFrontend>            _audio_frontend;\n\n    std::unique_ptr<midi_dispatcher::MidiDispatcher>     _midi_dispatcher;\n    std::unique_ptr<midi_frontend::ReactiveMidiFrontend> _midi_frontend;\n\n    RtSafeRtEventFifo _in_rt_queue;\n    RtSafeRtEventFifo _out_rt_queue;\n\n    Transport _transport {TEST_SAMPLE_RATE, &_out_rt_queue};\n\n    std::unique_ptr<sushi::internal::RtControllerAccessor> _accessor;\n};\n\nTEST_F(ReactiveControllerTestFrontend, TestRtControllerAudioCalls)\n{\n    ASSERT_FALSE(_mock_engine->process_called);\n\n    ChunkSampleBuffer in_buffer;\n    ChunkSampleBuffer out_buffer;\n\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n\n    _real_time_controller->process_audio(in_buffer, out_buffer, 1s);\n\n    test_utils::assert_buffer_value(1.0f, out_buffer);\n\n    ASSERT_TRUE(_mock_engine->process_called);\n}\n\nTEST_F(ReactiveControllerTestFrontend, TestRtControllerTransportCalls)\n{\n    // Tempo:\n    float old_tempo = _accessor->tempo();\n    float new_tempo = 124.5f;\n\n    _real_time_controller->set_tempo(new_tempo);\n\n    EXPECT_NE(old_tempo, new_tempo);\n    EXPECT_FLOAT_EQ(_accessor->tempo(), new_tempo);\n    EXPECT_FLOAT_EQ(_accessor->transport()->current_tempo(), new_tempo); // Since we don't gmock Transport.\n\n    // Time Signature:\n    auto old_time_signature = _accessor->time_signature();\n    control::TimeSignature new_time_signature {5, 8};\n    auto new_internal_time_signature = controller_impl::to_internal(new_time_signature);\n\n    _real_time_controller->set_time_signature(new_time_signature);\n\n    EXPECT_NE(old_time_signature, new_internal_time_signature);\n    EXPECT_EQ(_accessor->time_signature(), new_internal_time_signature);\n    EXPECT_EQ(_accessor->transport()->time_signature(),\n              new_internal_time_signature); // Since we don't gmock Transport.\n\n    // Playing Mode:\n    auto old_playing_mode = _accessor->playing_mode();\n    control::PlayingMode new_playing_mode = control::PlayingMode::PLAYING;\n    auto new_internal_playing_mode = controller_impl::to_internal(new_playing_mode);\n\n    _real_time_controller->set_playing_mode(new_playing_mode);\n\n    EXPECT_NE(old_playing_mode, new_playing_mode);\n    EXPECT_EQ(_accessor->playing_mode(), new_playing_mode);\n\n    // Only on set_time is playing_mode updated in Transport.\n    EXPECT_NE(_accessor->transport()->playing_mode(), new_internal_playing_mode);\n    _accessor->transport()->set_time(std::chrono::seconds(0), 0);\n    EXPECT_EQ(_accessor->transport()->playing_mode(), new_internal_playing_mode);\n\n    // Beat Count & Position Source (they interact):\n    auto old_beat_count = _accessor->transport()->current_beats();\n    double new_beat_count = 14.5;\n    _real_time_controller->set_current_beats(new_beat_count);\n\n    EXPECT_NE(new_beat_count, old_beat_count);\n    EXPECT_NE(new_beat_count, _accessor->transport()->current_beats());\n\n    _real_time_controller->set_position_source(TransportPositionSource::EXTERNAL);\n\n    _real_time_controller->set_current_beats(new_beat_count);\n\n    EXPECT_DOUBLE_EQ(new_beat_count, _accessor->transport()->current_beats());\n}\n\nTEST_F(ReactiveControllerTestFrontend, TestRtControllerMidiCalls)\n{\n    // TODO: Currently the Passive Controller MIDI handling over the Passive MIDI frontend, is unfinished,\n    //  and not real-time safe. Once it's finished (story AUD-456), we should add relevant tests also here.\n}\n"
  },
  {
    "path": "test/unittests/engine/controllers/session_controller_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"engine/controller/session_controller.cpp\"\n\n#include \"engine/audio_engine.h\"\n#include \"control_frontends/base_control_frontend.h\"\n#include \"test_utils/engine_mockup.h\"\n#include \"test_utils/audio_frontend_mockup.h\"\n#include \"plugins/equalizer_plugin.h\"\n#include \"control_frontends/osc_frontend.h\"\n#include \"test_utils/mock_osc_interface.h\"\n#include \"test_utils/control_mockup.h\"\n\nnamespace sushi::internal::engine::controller_impl\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(SessionController& f) : _friend(f) {}\n\n    [[nodiscard]] control::SushiBuildInfo save_build_info() const\n    {\n        return _friend._save_build_info();\n    }\n\n    [[nodiscard]] control::MidiState save_midi_state() const\n    {\n        return _friend._save_midi_state();\n    }\n\n    [[nodiscard]] control::EngineState save_engine_state() const\n    {\n        return _friend._save_engine_state();\n    }\n\n    [[nodiscard]] std::vector<control::TrackState> save_tracks() const\n    {\n        return _friend._save_tracks();\n    }\n\n    void clear_all_tracks()\n    {\n        _friend._clear_all_tracks();\n    }\n\nprivate:\n    SessionController& _friend;\n};\n\n} // end namespace sushi::internal::engine::controller_impl\n\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::control_frontend;\nusing namespace sushi::internal::midi_dispatcher;\nusing namespace sushi::internal::engine;\nusing namespace sushi::internal::engine::controller_impl;\n\nconstexpr float TEST_SAMPLE_RATE = 44100;\n\nclass SessionControllerTest : public ::testing::Test\n{\nprotected:\n    SessionControllerTest() = default;\n\n    void SetUp() override\n    {\n        _audio_engine = std::make_unique<AudioEngine>(TEST_SAMPLE_RATE, 1, \"\", false, new EventDispatcherMockup());\n        _mock_osc_interface = new MockOscInterface(0, 0, \"\");\n\n        _osc_frontend = std::make_unique<OSCFrontend>(_audio_engine.get(), &_mock_controller, _mock_osc_interface);\n        _event_dispatcher_mockup = static_cast<EventDispatcherMockup*>(_audio_engine->event_dispatcher());\n        _midi_dispatcher = std::make_unique<MidiDispatcher>(_event_dispatcher_mockup);\n        _module_under_test = std::make_unique<SessionController>(_audio_engine.get(), _midi_dispatcher.get(), &_audio_frontend, _event_dispatcher_mockup);\n        _module_under_test->set_osc_frontend(_osc_frontend.get());\n\n        _accessor = std::make_unique<sushi::internal::engine::controller_impl::Accessor>(*_module_under_test);\n\n        _audio_engine->set_audio_channels(8, 8);\n    }\n\n    MockOscInterface*                     _mock_osc_interface;\n    sushi::control::ControlMockup         _mock_controller;\n    EventDispatcherMockup*                _event_dispatcher_mockup;\n\n    AudioFrontendMockup                   _audio_frontend;\n    std::unique_ptr<AudioEngine>          _audio_engine;\n    std::unique_ptr<MidiDispatcher>       _midi_dispatcher;\n    std::unique_ptr<SessionController>    _module_under_test;\n    std::unique_ptr<OSCFrontend>          _osc_frontend;\n\n    std::unique_ptr<sushi::internal::engine::controller_impl::Accessor> _accessor;\n};\n\nTEST_F(SessionControllerTest, TestEmptyEngineState)\n{\n    auto state = _module_under_test->save_session();\n    EXPECT_FALSE(state.save_date.empty());\n    EXPECT_EQ(0u, state.tracks.size());\n}\n\nTEST_F(SessionControllerTest, TestSaveSushiInfo)\n{\n    auto info = _accessor->save_build_info();\n    EXPECT_NE(\"\", info.build_date);\n    EXPECT_NE(\"\", info.version);\n    EXPECT_NE(\"\", info.commit_hash);\n    EXPECT_GT(info.build_options.size(), 0u);\n    EXPECT_EQ(AUDIO_CHUNK_SIZE, info.audio_buffer_size);\n}\n\nTEST_F(SessionControllerTest, TestSaveOscState)\n{\n    // TODO : Test when OscFrontend can be suitably mocked\n}\n\nTEST_F(SessionControllerTest, TestSaveMidiState)\n{\n    std::string TRACK_NAME = \"track_1\";\n    std::string PROCESSOR_NAME = \"processor_1\";\n    constexpr int MIDI_PORT = 1;\n    constexpr int PARAMETER_ID = 1;\n    constexpr int CC_ID = 15;\n    constexpr MidiChannel MIDI_CH = MidiChannel::CH_10;\n    constexpr control::MidiChannel EXT_MIDI_CH = control::MidiChannel::MIDI_CH_10;\n\n    auto [track_status, track_id] = _audio_engine->create_track(TRACK_NAME, 2, std::optional<int>());\n    ASSERT_EQ(EngineReturnStatus::OK, track_status);\n\n    auto [status, proc_id] = _audio_engine->create_processor({.uid = std::string(equalizer_plugin::EqualizerPlugin::static_uid()),\n                                                                     .path = \"\",\n                                                                     .type = PluginType::INTERNAL},\n                                                             PROCESSOR_NAME);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    // Make some midi connections\n    _midi_dispatcher->set_midi_inputs(2);\n    _midi_dispatcher->set_midi_outputs(1);\n    ASSERT_EQ(MidiDispatcherStatus::OK, _midi_dispatcher->connect_raw_midi_to_track(MIDI_PORT, track_id));\n    ASSERT_EQ(MidiDispatcherStatus::OK, _midi_dispatcher->connect_cc_to_parameter(MIDI_PORT, proc_id, PARAMETER_ID, CC_ID, 0, 1, false, MIDI_CH));\n    ASSERT_EQ(MidiDispatcherStatus::OK, _midi_dispatcher->connect_pc_to_processor(MIDI_PORT, proc_id, MIDI_CH));\n\n    auto midi_state = _accessor->save_midi_state();\n\n    // Verify saved state\n    EXPECT_EQ(2, midi_state.inputs);\n    EXPECT_EQ(1, midi_state.outputs);\n    ASSERT_EQ(1u, midi_state.kbd_input_connections.size());\n    ASSERT_EQ(0u, midi_state.kbd_output_connections.size());\n    ASSERT_EQ(1u, midi_state.cc_connections.size());\n    ASSERT_EQ(1u, midi_state.pc_connections.size());\n\n    auto kbd_con = midi_state.kbd_input_connections.front();\n    EXPECT_TRUE(kbd_con.raw_midi);\n    EXPECT_EQ(TRACK_NAME, kbd_con.track);\n    EXPECT_EQ(MIDI_PORT, kbd_con.port);\n    EXPECT_EQ(control::MidiChannel::MIDI_CH_OMNI, kbd_con.channel);\n\n    auto cc_con = midi_state.cc_connections.front();\n    EXPECT_EQ(PROCESSOR_NAME, cc_con.processor);\n    EXPECT_EQ(PARAMETER_ID, cc_con.parameter_id);\n    EXPECT_EQ(MIDI_PORT, cc_con.port);\n    EXPECT_EQ(EXT_MIDI_CH, cc_con.channel);\n    EXPECT_EQ(CC_ID, cc_con.cc_number);\n    EXPECT_EQ(0.0, cc_con.min_range);\n    EXPECT_EQ(1.0, cc_con.max_range);\n    EXPECT_FALSE(cc_con.relative_mode);\n\n    auto pc_con = midi_state.pc_connections.front();\n    EXPECT_EQ(PROCESSOR_NAME, pc_con.processor);\n    EXPECT_EQ(MIDI_PORT, pc_con.port);\n    EXPECT_EQ(EXT_MIDI_CH, pc_con.channel);\n}\n\n\nTEST_F(SessionControllerTest, TestSaveEngineState)\n{\n    std::string TRACK_NAME = \"track_1\";\n\n    auto [track_status, track_id] = _audio_engine->create_track(TRACK_NAME, 2, std::optional<int>());\n    ASSERT_EQ(EngineReturnStatus::OK, track_status);\n\n    _audio_engine->set_audio_channels(8, 6);\n    _audio_engine->set_cv_input_channels(0);\n    _audio_engine->set_cv_output_channels(2);\n    _audio_engine->set_sample_rate(TEST_SAMPLE_RATE);\n    _audio_engine->set_tempo(125);\n    _audio_engine->set_tempo_sync_mode(SyncMode::MIDI);\n    _audio_engine->set_transport_mode(PlayingMode::STOPPED);\n    _audio_engine->set_time_signature({6, 8 });\n    _audio_engine->enable_input_clip_detection(true);\n    _audio_engine->enable_master_limiter(true);\n    ASSERT_EQ(EngineReturnStatus::OK, _audio_engine->connect_audio_input_channel(1, 1, track_id));\n    ASSERT_EQ(EngineReturnStatus::OK, _audio_engine->connect_audio_output_channel(2, 0, track_id));\n\n    auto engine_state = _accessor->save_engine_state();\n\n    // used_audio_in/outputs should reflect the minimum channels needed to restore the session\n    EXPECT_EQ(1, engine_state.used_audio_inputs);\n    EXPECT_EQ(2, engine_state.used_audio_outputs);\n    EXPECT_EQ(TEST_SAMPLE_RATE, engine_state.sample_rate);\n    EXPECT_EQ(125, engine_state.tempo);\n    EXPECT_EQ(control::PlayingMode::STOPPED, engine_state.playing_mode);\n    EXPECT_EQ(control::SyncMode::MIDI, engine_state.sync_mode);\n    EXPECT_EQ(6, engine_state.time_signature.numerator);\n    EXPECT_EQ(8, engine_state.time_signature.denominator);\n    EXPECT_TRUE(engine_state.input_clip_detection);\n    EXPECT_FALSE(engine_state.output_clip_detection);\n    EXPECT_TRUE(engine_state.master_limiter);\n    EXPECT_EQ(1u, engine_state.input_connections.size());\n    EXPECT_EQ(1u, engine_state.output_connections.size());\n\n    auto in_con = engine_state.input_connections.front();\n    EXPECT_EQ(TRACK_NAME, in_con.track);\n    EXPECT_EQ(1, in_con.engine_channel);\n    EXPECT_EQ(1, in_con.track_channel);\n\n    auto out_con = engine_state.output_connections.front();\n    EXPECT_EQ(TRACK_NAME, out_con.track);\n    EXPECT_EQ(2, out_con.engine_channel);\n    EXPECT_EQ(0, out_con.track_channel);\n}\n\nTEST_F(SessionControllerTest, TestSaveTracks)\n{\n    std::string TRACK_NAME = \"track_1\";\n    std::string PROCESSOR_NAME = \"processor_1\";\n\n    auto [track_status, track_id] = _audio_engine->create_track(TRACK_NAME, 2, std::nullopt);\n    ASSERT_EQ(EngineReturnStatus::OK, track_status);\n\n    auto [status, proc_id] = _audio_engine->create_processor({.uid = std::string(equalizer_plugin::EqualizerPlugin::static_uid()),\n                                                                     .path = \"\",\n                                                                     .type = PluginType::INTERNAL},\n                                                             PROCESSOR_NAME);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    ASSERT_EQ(EngineReturnStatus::OK, _audio_engine->add_plugin_to_track(proc_id, track_id));\n\n    auto tracks = _accessor->save_tracks();\n\n    ASSERT_EQ(1u, tracks.size());\n    auto& track = tracks.front();\n\n    EXPECT_EQ(TRACK_NAME, track.name);\n    EXPECT_EQ(\"\", track.label);\n    EXPECT_EQ(2, track.channels);\n    EXPECT_EQ(1, track.buses);\n    EXPECT_EQ(0, track.thread);\n    // Track has 3 parameters, gain, pan and mute. This is tested more thoroughly in the track tests\n    EXPECT_EQ(3u, track.track_state.parameters.size());\n\n    ASSERT_EQ(1u, track.processors.size());\n    auto& processor = track.processors.front();\n\n    EXPECT_EQ(PROCESSOR_NAME, processor.name);\n    EXPECT_EQ(\"Equalizer\", processor.label);\n    EXPECT_EQ(\"\", processor.path);\n    EXPECT_EQ(equalizer_plugin::EqualizerPlugin::static_uid(), processor.uid);\n    EXPECT_EQ(control::PluginType::INTERNAL, processor.type);\n    EXPECT_EQ(3u, processor.state.parameters.size());\n}\n\n\nTEST_F(SessionControllerTest, TestSaveAndRestore)\n{\n    std::string TRACK_NAME = \"track_1\";\n    std::string PROCESSOR_NAME = \"processor_1\";\n    constexpr int MIDI_PORT = 1;\n    constexpr int PARAMETER_ID = 1;\n    constexpr int CC_ID = 15;\n    constexpr MidiChannel MIDI_CH = MidiChannel::CH_10;\n\n    auto [track_status, track_id] = _audio_engine->create_track(TRACK_NAME, 2, std::optional<int>());\n    ASSERT_EQ(EngineReturnStatus::OK, track_status);\n\n    auto [status, proc_id] = _audio_engine->create_processor({.uid = std::string(equalizer_plugin::EqualizerPlugin::static_uid()),\n                                                                     .path = \"\",\n                                                                     .type = PluginType::INTERNAL},\n                                                             PROCESSOR_NAME);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    status = _audio_engine->add_plugin_to_track(proc_id, track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    // Make some midi connections\n    _midi_dispatcher->set_midi_inputs(2);\n    _midi_dispatcher->set_midi_outputs(1);\n    ASSERT_EQ(MidiDispatcherStatus::OK, _midi_dispatcher->connect_raw_midi_to_track(MIDI_PORT, track_id));\n    ASSERT_EQ(MidiDispatcherStatus::OK, _midi_dispatcher->connect_cc_to_parameter(MIDI_PORT, proc_id, PARAMETER_ID, CC_ID, 0, 1, false, MIDI_CH));\n    ASSERT_EQ(MidiDispatcherStatus::OK, _midi_dispatcher->connect_pc_to_processor(MIDI_PORT, proc_id, MIDI_CH));\n\n    // Set some engine parameters\n    _audio_engine->set_audio_channels(8, 6);\n    _audio_engine->set_cv_input_channels(0);\n    _audio_engine->set_cv_output_channels(2);\n    _audio_engine->set_sample_rate(TEST_SAMPLE_RATE);\n    _audio_engine->set_tempo(125);\n    _audio_engine->set_tempo_sync_mode(SyncMode::MIDI);\n    _audio_engine->set_transport_mode(PlayingMode::STOPPED);\n    _audio_engine->set_time_signature({6, 8 });\n    _audio_engine->enable_input_clip_detection(true);\n    _audio_engine->enable_master_limiter(true);\n\n    // Make some audio connections\n    ASSERT_EQ(EngineReturnStatus::OK, _audio_engine->connect_audio_input_channel(1, 1, track_id));\n    ASSERT_EQ(EngineReturnStatus::OK, _audio_engine->connect_audio_output_channel(2, 0, track_id));\n\n    // Save the state, clear all track and reload the state\n    auto session_state = _module_under_test->save_session();\n\n    _accessor->clear_all_tracks();\n    _midi_dispatcher->disconnect_all_cc_from_processor(proc_id);\n    _midi_dispatcher->disconnect_raw_midi_from_track(MIDI_PORT, track_id, MidiChannel::OMNI);\n    _midi_dispatcher->disconnect_track_from_output(MIDI_PORT, track_id, MIDI_CH);\n\n    ASSERT_TRUE(_audio_engine->processor_container()->all_tracks().empty());\n\n    auto controller_response = _module_under_test->restore_session(session_state);\n    // As the restore is done asynchronously, we need to execute the event manually\n\n    _event_dispatcher_mockup->execute_engine_event(_audio_engine.get());\n\n    // Check that tracks and processors were restored correctly\n    auto processors = _audio_engine->processor_container();\n    ASSERT_EQ(control::ControlStatus::ASYNC_RESPONSE, controller_response.status);\n    ASSERT_EQ(1, processors->all_tracks().size());\n    auto restored_track = processors->all_tracks().front();\n\n    ASSERT_EQ(1, processors->processors_on_track(restored_track->id()).size());\n    auto restored_plugin = processors->processors_on_track(restored_track->id()).front();\n\n    EXPECT_EQ(session_state.tracks.front().name, restored_track->name());\n    EXPECT_EQ(session_state.tracks.front().label, restored_track->label());\n    EXPECT_EQ(session_state.tracks.front().channels, restored_track->input_channels());\n    EXPECT_EQ(session_state.tracks.front().channels, restored_track->output_channels());\n    EXPECT_EQ(session_state.tracks.front().buses, restored_track->buses());\n    EXPECT_EQ(session_state.tracks.front().track_state.parameters[0].second, restored_track->parameter_value(0).second);\n\n    EXPECT_EQ(session_state.tracks.front().processors.front().name, restored_plugin->name());\n    EXPECT_EQ(session_state.tracks.front().processors.front().label, restored_plugin->label());\n    EXPECT_EQ(session_state.tracks.front().processors.front().state.parameters[0].second, restored_plugin->parameter_value(0).second);\n\n    // Check that engine was restored correctly\n    ASSERT_EQ(1, _audio_engine->audio_input_connections().size());\n    ASSERT_EQ(1, _audio_engine->audio_output_connections().size());\n    auto connection = _audio_engine->audio_input_connections().front();\n    EXPECT_EQ(1, connection.engine_channel);\n    EXPECT_EQ(1, connection.track_channel);\n    EXPECT_EQ(restored_track->id(), connection.track);\n    connection = _audio_engine->audio_output_connections().front();\n    EXPECT_EQ(2, connection.engine_channel);\n    EXPECT_EQ(0, connection.track_channel);\n    EXPECT_EQ(restored_track->id(), connection.track);\n\n    EXPECT_EQ(TEST_SAMPLE_RATE, _audio_engine->sample_rate());\n    EXPECT_EQ(125, _audio_engine->transport()->current_tempo());\n    EXPECT_EQ(SyncMode::MIDI, _audio_engine->transport()->sync_mode());\n    EXPECT_EQ(PlayingMode::STOPPED, _audio_engine->transport()->playing_mode());\n    EXPECT_EQ(TimeSignature({6, 8}), _audio_engine->transport()->time_signature());\n\n    EXPECT_TRUE(_audio_engine->master_limiter());\n    EXPECT_TRUE(_audio_engine->input_clip_detection());\n    EXPECT_FALSE(_audio_engine->output_clip_detection());\n\n    // Check midi connections\n    auto kbd_in_routes = _midi_dispatcher->get_all_kb_input_connections();\n    ASSERT_EQ(1u, kbd_in_routes.size());\n    EXPECT_EQ(restored_track->id(), kbd_in_routes.front().input_connection.target);\n    EXPECT_TRUE(kbd_in_routes.front().raw_midi);\n    EXPECT_EQ(MIDI_PORT, kbd_in_routes.front().port);\n    EXPECT_EQ(MidiChannel::OMNI, kbd_in_routes.front().channel);\n\n    ASSERT_EQ(0u, _midi_dispatcher->get_all_kb_output_connections().size());\n\n    auto cc_routes = _midi_dispatcher->get_cc_input_connections_for_processor(restored_plugin->id());\n    ASSERT_EQ(1u, cc_routes.size());\n    EXPECT_EQ(MIDI_PORT, cc_routes.front().port);\n    EXPECT_EQ(CC_ID, cc_routes.front().cc);\n    EXPECT_EQ(MIDI_CH, cc_routes.front().channel);\n\n    auto pc_routes = _midi_dispatcher->get_pc_input_connections_for_processor(restored_plugin->id());\n    ASSERT_EQ(1u, pc_routes.size());\n    EXPECT_EQ(MIDI_PORT, pc_routes.front().port);\n    EXPECT_EQ(MIDI_CH, pc_routes.front().channel);\n}"
  },
  {
    "path": "test/unittests/engine/engine_test.cpp",
    "content": "#include <thread>\n\n#include \"gtest/gtest.h\"\n\n#include \"test_utils/plugin_accessors.h\"\n#include \"test_utils/test_utils.h\"\n#include \"engine/transport.h\"\n\n#include \"plugins/equalizer_plugin.h\"\n#include \"engine/audio_engine.cpp\"\n#include \"library/internal_processor_factory.cpp\"\n#include \"library/plugin_registry.cpp\"\n#include \"test_utils/dummy_processor.h\"\n\n#include \"test_utils/audio_graph_accessor.h\"\n#include \"test_utils/track_accessor.h\"\n\nnamespace sushi::internal::engine\n{\n\nclass AudioEngineAccessor\n{\npublic:\n    explicit AudioEngineAccessor(AudioEngine& f) : _friend(f) {}\n\n    [[nodiscard]] AudioGraph& audio_graph()\n    {\n        return _friend._audio_graph;\n    }\n\n    [[nodiscard]] ProcessorContainer& processors()\n    {\n        return _friend._processors;\n    }\n\n    [[nodiscard]] std::vector<Processor*>& realtime_processors()\n    {\n        return _friend._realtime_processors;\n    }\n\n    void remove_connections_from_track(ObjectId track_id)\n    {\n        _friend._remove_connections_from_track(track_id);\n    }\n\nprivate:\n    AudioEngine& _friend;\n};\n\n}\n\nconstexpr float SAMPLE_RATE = 44000;\nconstexpr int TEST_CHANNEL_COUNT = 4;\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\n\n\n\nclass TestClipDetector : public ::testing::Test\n{\nprotected:\n    TestClipDetector() = default;\n\n    void SetUp() override\n    {\n        _module_under_test.set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n    }\n\n    ClipDetector _module_under_test{SAMPLE_RATE};\n};\n\nTEST_F(TestClipDetector, TestClipping)\n{\n    RtSafeRtEventFifo queue;\n    ChunkSampleBuffer buffer(TEST_CHANNEL_COUNT);\n    test_utils::fill_sample_buffer(buffer, 0.5f);\n    _module_under_test.detect_clipped_samples(buffer, queue, false);\n    /* No samples outside (-1.0, 1.0) so this should result in no notifications */\n    ASSERT_TRUE(queue.empty());\n\n    /* Set 2 samples to clipped, we should now have 2 clip notifications */\n    buffer.channel(1)[10] = 1.5f;\n    buffer.channel(3)[6] = -1.3f;\n    _module_under_test.detect_clipped_samples(buffer, queue, false);\n    ASSERT_FALSE(queue.empty());\n    RtEvent notification;\n    ASSERT_TRUE(queue.pop(notification));\n    ASSERT_EQ(1, notification.clip_notification_event()->channel());\n    ASSERT_EQ(ClipNotificationRtEvent::ClipChannelType::OUTPUT, notification.clip_notification_event()->channel_type());\n    ASSERT_TRUE(queue.pop(notification));\n    ASSERT_EQ(3, notification.clip_notification_event()->channel());\n    ASSERT_EQ(ClipNotificationRtEvent::ClipChannelType::OUTPUT, notification.clip_notification_event()->channel_type());\n\n    /* But calling again immediately should not trigger due to the rate limiting */\n    _module_under_test.detect_clipped_samples(buffer, queue, false);\n    ASSERT_TRUE(queue.empty());\n\n    /* But calling with audio_input set to true should trigger 2 new */\n    _module_under_test.detect_clipped_samples(buffer, queue, true);\n    ASSERT_FALSE(queue.empty());\n    ASSERT_TRUE(queue.pop(notification));\n    ASSERT_EQ(ClipNotificationRtEvent::ClipChannelType::INPUT, notification.clip_notification_event()->channel_type());\n    ASSERT_TRUE(queue.pop(notification));\n    ASSERT_FALSE(queue.pop(notification));\n}\n\n/*\n* Engine tests\n*/\nclass TestEngine : public ::testing::Test\n{\nprotected:\n    TestEngine() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<AudioEngine>(SAMPLE_RATE, 1);\n        _module_under_test->set_audio_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n\n        _accessor = std::make_unique<sushi::internal::engine::AudioEngineAccessor>(*_module_under_test);\n\n        _processors = _module_under_test->processor_container();\n    }\n\n    std::unique_ptr<AudioEngine> _module_under_test;\n\n    std::unique_ptr<sushi::internal::engine::AudioEngineAccessor> _accessor;\n\n    const BaseProcessorContainer* _processors {};\n};\n\n/*\n * Test that 1:s in gives 1:s out\n */\nTEST_F(TestEngine, TestProcess)\n{\n    /* Add a plugin track and connect it to inputs and outputs */\n    auto [status, track_id] = _module_under_test->create_track(\"test_track\", 2, std::nullopt);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    auto track = _module_under_test->processor_container()->track(\"test_track\");\n    ASSERT_NE(nullptr, track);\n\n    auto res = _module_under_test->connect_audio_input_bus(0, 0, track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, res);\n    res = _module_under_test->connect_audio_output_bus(0, 0, track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, res);\n\n    /* Run tests */\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(TEST_CHANNEL_COUNT);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(TEST_CHANNEL_COUNT);\n    ControlBuffer control_buffer;\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n    test_utils::fill_sample_buffer(out_buffer, 0.5f);\n\n    _module_under_test->process_chunk(&in_buffer, &out_buffer, &control_buffer, &control_buffer, Time(0), 0);\n\n    /* Separate the first 2 channels, which should pass through unprocessed\n     * and the 2 last, which should be set to 0 since they are not connected to anything */\n    auto main_bus = SampleBuffer<AUDIO_CHUNK_SIZE>::create_non_owning_buffer(out_buffer, 0, 2);\n    auto second_bus = SampleBuffer<AUDIO_CHUNK_SIZE>::create_non_owning_buffer(out_buffer, 2, 2);\n\n    test_utils::assert_buffer_value(1.0f, main_bus, test_utils::DECIBEL_ERROR);\n    test_utils::assert_buffer_value(0.0f, second_bus, test_utils::DECIBEL_ERROR);\n\n    /* Add a plugin to the track and do the same thing */\n    PluginInfo plugin_info;\n    plugin_info.uid = \"sushi.testing.gain\";\n    plugin_info.path = \"\";\n    plugin_info.type = PluginType::INTERNAL;\n\n    auto [load_status, plugin_id] = _module_under_test->create_processor(plugin_info, \"gain\");\n    ASSERT_EQ(EngineReturnStatus::OK, load_status);\n    res = _module_under_test->add_plugin_to_track(plugin_id, track->id());\n    ASSERT_EQ(EngineReturnStatus::OK, res);\n\n    _module_under_test->process_chunk(&in_buffer, &out_buffer, &control_buffer, &control_buffer, Time(0), 0);\n    main_bus = SampleBuffer<AUDIO_CHUNK_SIZE>::create_non_owning_buffer(out_buffer, 0, 2);\n\n    test_utils::assert_buffer_value(1.0f, main_bus, test_utils::DECIBEL_ERROR);\n}\n\nTEST_F(TestEngine, TestOutputMixing)\n{\n    auto [status_1, track_1_id] = _module_under_test->create_track(\"1\", 2, std::nullopt);\n    auto [status_2, track_2_id] = _module_under_test->create_track(\"2\", 2, std::nullopt);\n    ASSERT_EQ(EngineReturnStatus::OK, status_1);\n    ASSERT_EQ(EngineReturnStatus::OK, status_2);\n    _module_under_test->connect_audio_input_bus(0, 0, track_1_id);\n    _module_under_test->connect_audio_input_bus(1, 0, track_2_id);\n    _module_under_test->connect_audio_output_bus(0, 0, track_1_id);\n    _module_under_test->connect_audio_output_bus(0, 0, track_2_id);\n\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(TEST_CHANNEL_COUNT);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(TEST_CHANNEL_COUNT);\n    ControlBuffer control_buffer;\n\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n\n    _module_under_test->process_chunk(&in_buffer, &out_buffer, &control_buffer, &control_buffer, Time(0), 0);\n\n    /* Both track's outputs are routed to bus 0, so they should sum to 2 */\n    auto main_bus = SampleBuffer<AUDIO_CHUNK_SIZE>::create_non_owning_buffer(out_buffer, 0, 2);\n\n    test_utils::assert_buffer_value(2.0f, main_bus, test_utils::DECIBEL_ERROR);\n}\n\nTEST_F(TestEngine, TestCreateEmptyTrack)\n{\n    auto [status, track_id] = _module_under_test->create_track(\"left\", 2, std::nullopt);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    ASSERT_TRUE(_processors->processor_exists(\"left\"));\n    auto left_track_id = track_id;\n\n    AudioGraph& audio_graph = _accessor->audio_graph();\n    AudioGraphAccessor _ag_accessor {audio_graph};\n\n    ASSERT_EQ(_ag_accessor.audio_graph()[0].tracks.size(), 1u);\n    ASSERT_EQ(_ag_accessor.audio_graph()[0].tracks[0]->name(), \"left\");\n\n    /* Test invalid name */\n    std::tie(status, track_id) = _module_under_test->create_track(\"left\", 1, std::nullopt);\n    ASSERT_EQ(EngineReturnStatus::INVALID_PROCESSOR, status);\n    std::tie(status, track_id) = _module_under_test->create_track(\"\", 1, std::nullopt);\n    ASSERT_EQ(EngineReturnStatus::INVALID_PLUGIN, status);\n\n    /* Test removal */\n    status = _module_under_test->delete_track(left_track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    ASSERT_FALSE(_processors->processor_exists(\"left\"));\n    ASSERT_EQ(_ag_accessor.audio_graph()[0].tracks.size(), 0u);\n\n    /* Test invalid number of channels */\n    std::tie(status, track_id) = _module_under_test->create_track(\"left\", MAX_TRACK_CHANNELS + 1, std::nullopt);\n    ASSERT_EQ(status, EngineReturnStatus::INVALID_N_CHANNELS);\n}\n\nTEST_F(TestEngine, TestCreatePreAndPostTracks)\n{\n    auto [status, track_id] = _module_under_test->create_pre_track(\"pre\");\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    auto track = _processors->track(\"pre\");\n    ASSERT_TRUE(track);\n    ASSERT_EQ(TrackType::PRE, track->type());\n    ASSERT_EQ(track_id, track->id());\n\n    std::tie(status, track_id) = _module_under_test->create_post_track(\"post\");\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    track = _processors->track(\"post\");\n    ASSERT_TRUE(track);\n    ASSERT_EQ(TrackType::POST, track->type());\n    ASSERT_EQ(track_id, track->id());\n\n    /* Test creating a second post track, this should fail */\n    std::tie(status, track_id) = _module_under_test->create_post_track(\"post\");\n    ASSERT_NE(EngineReturnStatus::OK, status);\n}\n\n#ifndef _MSC_VER\n// Only run this test on platforms where we have twine::WorkerPool with multithreading support\nTEST_F(TestEngine, TestCreateTrackOnThread)\n{\n    _module_under_test = std::make_unique<AudioEngine>(SAMPLE_RATE, 2);\n    _processors = _module_under_test->processor_container();\n\n    auto [track_1_status, track_1_id] = _module_under_test->create_track(\"main\", 2, 1);\n    auto [track_2_status, track_2_id] = _module_under_test->create_track(\"two\", 2, std::nullopt);\n\n    ASSERT_EQ(EngineReturnStatus::OK, track_1_status);\n    ASSERT_EQ(EngineReturnStatus::OK, track_2_status);\n\n    auto track_1 = _processors->track(track_1_id);\n    auto track_2 = _processors->track(track_2_id);\n\n    EXPECT_EQ(1, track_1->thread());\n    EXPECT_EQ(0, track_2->thread());\n}\n#endif\n\nTEST_F(TestEngine, TestAddAndRemovePlugin)\n{\n    /* Test adding Internal plugins */\n    auto [left_track_status, left_track_id] = _module_under_test->create_track(\"left\", 2, std::nullopt);\n    ASSERT_EQ(EngineReturnStatus::OK, left_track_status);\n\n    PluginInfo gain_plugin_info;\n    gain_plugin_info.uid = \"sushi.testing.gain\";\n    gain_plugin_info.path = \"\";\n    gain_plugin_info.type = PluginType::INTERNAL;\n\n    auto [gain_status, gain_id] = _module_under_test->create_processor(gain_plugin_info, \"gain\");\n    ASSERT_EQ(gain_status, EngineReturnStatus::OK);\n\n    PluginInfo synth_plugin_info;\n    synth_plugin_info.uid = \"sushi.testing.sampleplayer\";\n    synth_plugin_info.path = \"\";\n    synth_plugin_info.type = PluginType::INTERNAL;\n\n    auto [synth_status, synth_id] = _module_under_test->create_processor(synth_plugin_info, \"synth\");\n    ASSERT_EQ(synth_status, EngineReturnStatus::OK);\n\n    auto status = _module_under_test->add_plugin_to_track(gain_id, left_track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    /* Add synth before gain */\n    status = _module_under_test->add_plugin_to_track(synth_id, left_track_id, gain_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    auto processors = _processors->processors_on_track(left_track_id);\n    ASSERT_EQ(2u, processors.size());\n    ASSERT_EQ(\"synth\", processors.front()->name());\n\n    /* Check that processors exists and in the right order on track \"main\" */\n    ASSERT_TRUE(_processors->processor_exists(\"gain\"));\n    ASSERT_TRUE(_processors->processor_exists(\"synth\"));\n\n    AudioGraph& audio_graph = _accessor->audio_graph();\n    AudioGraphAccessor _ag_accessor {audio_graph};\n    TrackAccessor _track_accessor_0 {*(_ag_accessor.audio_graph()[0].tracks[0])};\n\n    ASSERT_EQ(2u, _track_accessor_0.processors().size());\n    ASSERT_EQ(\"synth\", _track_accessor_0.processors()[0]->name());\n    ASSERT_EQ(\"gain\", _track_accessor_0.processors()[1]->name());\n\n    /* Move a processor from 1 track to another */\n    auto [right_track_status, right_track_id] = _module_under_test->create_track(\"right\", 2, std::nullopt);\n    ASSERT_EQ(EngineReturnStatus::OK, right_track_status);\n    status = _module_under_test->remove_plugin_from_track(synth_id, left_track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    status = _module_under_test->add_plugin_to_track(synth_id, right_track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    auto left_processors = _processors->processors_on_track(left_track_id);\n    auto right_processors = _processors->processors_on_track(right_track_id);\n    ASSERT_EQ(1u, left_processors.size());\n    ASSERT_EQ(\"gain\", left_processors.front()->name());\n    ASSERT_EQ(1u, right_processors.size());\n    ASSERT_EQ(\"synth\", right_processors.front()->name());\n\n    /* Test removing plugin */\n    status = _module_under_test->remove_plugin_from_track(gain_id, left_track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    processors = _processors->processors_on_track(left_track_id);\n    ASSERT_EQ(0u, processors.size());\n\n    status = _module_under_test->delete_plugin(gain_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    TrackAccessor _track_accessor_1 {*(_ag_accessor.audio_graph()[0].tracks[1])};\n\n    ASSERT_FALSE(_processors->processor_exists(\"gain\"));\n    ASSERT_EQ(0u, _track_accessor_0.processors().size());\n    ASSERT_EQ(\"synth\", _track_accessor_1.processors()[0]->name());\n\n    /* Negative tests */\n    ObjectId id;\n\n    PluginInfo plugin_info;\n    plugin_info.uid = \"sushi.testing.passthrough\";\n    plugin_info.path = \"\";\n    plugin_info.type = PluginType::INTERNAL;\n\n    std::tie(status, id) = _module_under_test->create_processor(plugin_info, \"dummyname\");\n\n    status = _module_under_test->add_plugin_to_track(ObjectId(123), ObjectId(456));\n    ASSERT_EQ(EngineReturnStatus::INVALID_TRACK, status);\n\n    std::tie(status, id) = _module_under_test->create_processor(plugin_info, \"\");\n\n    ASSERT_EQ(EngineReturnStatus::INVALID_PLUGIN, status);\n\n    plugin_info.uid = \"not_found\";\n    plugin_info.path = \"\";\n    plugin_info.type = PluginType::INTERNAL;\n\n    std::tie(status, id) = _module_under_test->create_processor(plugin_info, \"\");\n    ASSERT_EQ(EngineReturnStatus::ERROR, status);\n\n    plugin_info.uid = \"not_found\";\n    plugin_info.path = \"\";\n    plugin_info.type = PluginType::VST2X;\n    std::tie(status, id) = _module_under_test->create_processor(plugin_info, \"dummyname\");\n    ASSERT_NE(EngineReturnStatus::OK, status);\n\n    status = _module_under_test->remove_plugin_from_track(ObjectId(345), left_track_id);\n    ASSERT_EQ(EngineReturnStatus::INVALID_PLUGIN, status);\n}\n\nTEST_F(TestEngine, TestSetSamplerate)\n{\n    auto [track_status, track_id] = _module_under_test->create_track(\"left\", 2, std::nullopt);\n    ASSERT_EQ(EngineReturnStatus::OK, track_status);\n\n    PluginInfo plugin_info;\n    plugin_info.uid = \"sushi.testing.equalizer\";\n    plugin_info.path = \"\";\n    plugin_info.type = PluginType::INTERNAL;\n\n    auto [load_status, id] = _module_under_test->create_processor(plugin_info, \"eq\");\n\n    ASSERT_EQ(EngineReturnStatus::OK, load_status);\n    auto status = _module_under_test->add_plugin_to_track(id, _processors->track(\"left\")->id());\n    ASSERT_GE(EngineReturnStatus::OK, status);\n\n    _module_under_test->set_sample_rate(48000.0f);\n    ASSERT_FLOAT_EQ(48000.0f, _module_under_test->sample_rate());\n    /* Pretty ugly way of checking that it was actually set, but wth */\n    auto eq_plugin = static_cast<const equalizer_plugin::EqualizerPlugin*>(_accessor->processors().processor(\"eq\").get());\n\n    sushi::internal::equalizer_plugin::Accessor eq_plugin_accessor {eq_plugin};\n\n    ASSERT_FLOAT_EQ(48000.0f, eq_plugin_accessor.const_sample_rate());\n}\n\nTEST_F(TestEngine, TestRealtimeConfiguration)\n{\n    auto faux_rt_thread = [](AudioEngine* e)\n    {\n        SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(2);\n        SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(2);\n        ControlBuffer control_buffer;\n        std::this_thread::sleep_for(std::chrono::milliseconds(1));\n        e->process_chunk(&in_buffer, &out_buffer, &control_buffer, &control_buffer, Time(0), 0);\n    };\n\n    // Add a track, then a plugin to it while the engine is running, i.e. do it by asynchronous events instead\n    _module_under_test->enable_realtime(true);\n    auto rt = std::thread(faux_rt_thread, _module_under_test.get());\n    auto [track_status, track_id] = _module_under_test->create_track(\"main\", 2, std::nullopt);\n    rt.join();\n    ASSERT_EQ(EngineReturnStatus::OK, track_status);\n\n    rt = std::thread(faux_rt_thread, _module_under_test.get());\n\n    PluginInfo gain_plugin_info;\n    gain_plugin_info.uid = \"sushi.testing.gain\";\n    gain_plugin_info.path = \"\";\n    gain_plugin_info.type = PluginType::INTERNAL;\n\n    auto [load_status, plugin_id] = _module_under_test->create_processor(gain_plugin_info, \"gain_0_r\");\n    rt.join();\n    ASSERT_EQ(EngineReturnStatus::OK, load_status);\n\n    rt = std::thread(faux_rt_thread, _module_under_test.get());\n    auto status = _module_under_test->add_plugin_to_track(plugin_id, track_id);\n    rt.join();\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    AudioGraph& audio_graph = _accessor->audio_graph();\n    AudioGraphAccessor _ag_accessor {audio_graph};\n    TrackAccessor _track_accessor_0 {*(_ag_accessor.audio_graph()[0].tracks[0])};\n\n    ASSERT_EQ(1u, _track_accessor_0.processors().size());\n\n    // Remove the plugin and track.\n\n    // Deleting the plugin before removing it from the track should return an error\n    status = _module_under_test->delete_plugin(plugin_id);\n    ASSERT_EQ(EngineReturnStatus::ERROR, status);\n\n    rt = std::thread(faux_rt_thread, _module_under_test.get());\n    status = _module_under_test->remove_plugin_from_track(plugin_id, track_id);\n    rt.join();\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    ASSERT_EQ(0u, _track_accessor_0.processors().size());\n\n    rt = std::thread(faux_rt_thread, _module_under_test.get());\n    status = _module_under_test->delete_plugin(plugin_id);\n    rt.join();\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    rt = std::thread(faux_rt_thread, _module_under_test.get());\n    status = _module_under_test->delete_track(track_id);\n    rt.join();\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    ASSERT_EQ(0u, _module_under_test->audio_input_connections().size());\n\n    // Assert that they were also deleted from the map of processors\n    ASSERT_FALSE(_processors->processor_exists(\"main\"));\n    ASSERT_FALSE(_processors->processor_exists(\"gain_0_r\"));\n    ASSERT_FALSE(_processors->processor_exists(plugin_id));\n    ASSERT_FALSE(_processors->processor_exists(track_id));\n    ASSERT_FALSE(_accessor->realtime_processors()[track_id]);\n    ASSERT_FALSE(_accessor->realtime_processors()[plugin_id]);\n}\n\nTEST_F(TestEngine, TestAudioConnections)\n{\n    auto faux_rt_thread = [](AudioEngine* e, ChunkSampleBuffer* in, ChunkSampleBuffer* out, ControlBuffer* ctrl)\n    {\n        std::this_thread::sleep_for(std::chrono::milliseconds(1));\n        e->process_chunk(in, out, ctrl, ctrl, Time(0), 0);\n    };\n\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(4);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(4);\n    ControlBuffer control_buffer;\n\n    // Fill the channels with different values, so we can differentiate channels\n    for (int i = 0; i < in_buffer.channel_count(); ++i)\n    {\n        auto channel_buffer = ChunkSampleBuffer::create_non_owning_buffer(in_buffer, i, 1);\n        test_utils::fill_sample_buffer(channel_buffer, static_cast<float>(i + 1));\n    }\n\n    // Create a track and connect audio channels\n    auto [track_status, track_id] = _module_under_test->create_track(\"main\", 2, std::nullopt);\n    ASSERT_EQ(EngineReturnStatus::OK, track_status);\n    auto status = _module_under_test->connect_audio_input_channel(0, 0, track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    status = _module_under_test->connect_audio_output_channel(1, 0, track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    EXPECT_EQ(1u, _module_under_test->audio_input_connections().size());\n    EXPECT_EQ(1u, _module_under_test->audio_output_connections().size());\n\n    _module_under_test->process_chunk(&in_buffer, &out_buffer, &control_buffer, &control_buffer, Time(0), 0);\n    EXPECT_FLOAT_EQ(0.0, out_buffer.channel(0)[0]);\n    EXPECT_FLOAT_EQ(1.0, out_buffer.channel(1)[0]);\n    EXPECT_FLOAT_EQ(0.0, out_buffer.channel(2)[0]);\n    EXPECT_FLOAT_EQ(0.0, out_buffer.channel(3)[0]);\n\n    // Connect some while the engine is running\n    _module_under_test->enable_realtime(true);\n    auto rt = std::thread(faux_rt_thread, _module_under_test.get(), &in_buffer, &out_buffer, &control_buffer);\n    status = _module_under_test->connect_audio_input_channel(3, 1, track_id);\n    rt.join();\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    rt = std::thread(faux_rt_thread, _module_under_test.get(), &in_buffer, &out_buffer, &control_buffer);\n    status = _module_under_test->connect_audio_output_channel(2, 1, track_id);\n    rt.join();\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    EXPECT_FLOAT_EQ(0.0, out_buffer.channel(0)[0]);\n    EXPECT_FLOAT_EQ(1.0, out_buffer.channel(1)[0]);\n    EXPECT_FLOAT_EQ(4.0, out_buffer.channel(2)[0]);\n    EXPECT_FLOAT_EQ(0.0, out_buffer.channel(3)[0]);\n\n    // Remove the connections\n    _module_under_test->enable_realtime(false);\n    rt = std::thread(faux_rt_thread, _module_under_test.get(), &in_buffer, &out_buffer, &control_buffer);\n\n    _accessor->remove_connections_from_track(track_id);\n\n    rt.join();\n    EXPECT_EQ(0u, _module_under_test->audio_input_connections().size());\n    EXPECT_EQ(0u, _module_under_test->audio_output_connections().size());\n}\n\nTEST_F(TestEngine, TestSetCvChannels)\n{\n    EXPECT_EQ(EngineReturnStatus::OK, _module_under_test->set_cv_input_channels(2));\n    EXPECT_EQ(EngineReturnStatus::OK, _module_under_test->set_cv_output_channels(2));\n    // Set too many or route to non-existing inputs/processors\n    EXPECT_NE(EngineReturnStatus::OK, _module_under_test->set_cv_input_channels(20));\n    EXPECT_NE(EngineReturnStatus::OK, _module_under_test->set_cv_output_channels(20));\n\n    EXPECT_NE(EngineReturnStatus::OK, _module_under_test->connect_cv_to_parameter(\"proc\", \"param\", 1));\n    EXPECT_NE(EngineReturnStatus::OK, _module_under_test->connect_cv_from_parameter(\"proc\", \"param\", 1));\n}\n\nTEST_F(TestEngine, TestCvRouting)\n{\n    /* Add a control plugin track and connect cv to its parameters */\n    auto [track_status, track_id] = _module_under_test->create_track(\"lfo_track\", 0, std::nullopt);\n\n    PluginInfo lfo_plugin_info;\n    lfo_plugin_info.uid = \"sushi.testing.lfo\";\n    lfo_plugin_info.path = \"\";\n    lfo_plugin_info.type = PluginType::INTERNAL;\n\n    auto [status, id] = _module_under_test->create_processor(lfo_plugin_info, \"lfo\");\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    status = _module_under_test->add_plugin_to_track(id, track_id);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    status = _module_under_test->set_cv_input_channels(2);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    status = _module_under_test->set_cv_output_channels(2);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    status = _module_under_test->connect_cv_to_parameter(\"lfo\", \"freq\", 1);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    // First try with a too high cv input id\n    status = _module_under_test->connect_cv_from_parameter(\"lfo\", \"out\", 10);\n    ASSERT_NE(EngineReturnStatus::OK, status);\n    status = _module_under_test->connect_cv_from_parameter(\"lfo\", \"out\", 1);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    ChunkSampleBuffer in_buffer(1);\n    ChunkSampleBuffer out_buffer(1);\n    ControlBuffer in_controls;\n    ControlBuffer out_controls;\n\n    in_controls.cv_values[1] = 0.5;\n    _module_under_test->process_chunk(&in_buffer, &out_buffer, &in_controls, &out_controls, Time(0), 0);\n    _module_under_test->process_chunk(&in_buffer, &out_buffer, &in_controls, &out_controls, Time(0), 0);\n\n    // We should have a non-zero value in this slot\n    ASSERT_NE(0.0f, out_controls.cv_values[1]);\n}\n\nTEST_F(TestEngine, TestGateRouting)\n{\n    /* Build a cv/gate to midi to cv/gate chain and verify gate changes travel through it*/\n    _module_under_test->create_track(\"cv\", 0, std::nullopt);\n\n    PluginInfo cv_to_control_plugin_info;\n    cv_to_control_plugin_info.uid = \"sushi.testing.cv_to_control\";\n    cv_to_control_plugin_info.path = \"\";\n    cv_to_control_plugin_info.type = PluginType::INTERNAL;\n    auto [cv_ctrl_status, cv_ctrl_id] = _module_under_test->create_processor(cv_to_control_plugin_info, \"cv_ctrl\");\n\n    ASSERT_EQ(EngineReturnStatus::OK, cv_ctrl_status);\n    auto status = _module_under_test->add_plugin_to_track(cv_ctrl_id, _processors->track(\"cv\")->id());\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    PluginInfo control_to_cv_plugin_info;\n    control_to_cv_plugin_info.uid = \"sushi.testing.control_to_cv\";\n    control_to_cv_plugin_info.path = \"\";\n    control_to_cv_plugin_info.type = PluginType::INTERNAL;\n    auto [ctrl_cv_id_status, ctrl_cv_id] = _module_under_test->create_processor(control_to_cv_plugin_info, \"ctrl_cv\");\n\n    ASSERT_EQ(EngineReturnStatus::OK, ctrl_cv_id_status);\n    status = _module_under_test->add_plugin_to_track(ctrl_cv_id, _processors->track(\"cv\")->id());\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    status = _module_under_test->set_cv_input_channels(2);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n    status = _module_under_test->set_cv_output_channels(2);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    status = _module_under_test->connect_gate_to_processor(\"cv_ctrl\", 1, 0, 0);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    status = _module_under_test->connect_gate_from_processor(\"ctrl_cv\", 0, 0, 0);\n    ASSERT_EQ(EngineReturnStatus::OK, status);\n\n    ChunkSampleBuffer in_buffer(1);\n    ChunkSampleBuffer out_buffer(1);\n    ControlBuffer in_controls;\n    ControlBuffer out_controls;\n    in_controls.gate_values.reset();\n    in_controls.gate_values[1] = true;\n\n    _module_under_test->process_chunk(&in_buffer, &out_buffer, &in_controls, &out_controls, Time(0), 0);\n    // A gate high event on gate input 1 should result in a gate high on gate output 0\n    ASSERT_TRUE(out_controls.gate_values[0]);\n    ASSERT_EQ(1u, out_controls.gate_values.count());\n}\n\nTEST_F(TestEngine, TestMasterTrackProcessing)\n{\n    constexpr float GAIN_6DB = 126.0 / 144;\n\n    ChunkSampleBuffer in_buffer(TEST_CHANNEL_COUNT);\n    ChunkSampleBuffer out_buffer(TEST_CHANNEL_COUNT);\n    ControlBuffer ctrl_buffer;\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n\n    auto [empty_status, empty_track_id] = _module_under_test->create_track(\"empty\", TEST_CHANNEL_COUNT, std::nullopt);\n    ASSERT_EQ(EngineReturnStatus::OK, empty_status);\n\n    auto [pre_status, pre_track_id] = _module_under_test->create_pre_track(\"pre\");\n    ASSERT_EQ(EngineReturnStatus::OK, pre_status);\n\n    auto [post_status, post_track_id] = _module_under_test->create_post_track(\"post\");\n    ASSERT_EQ(EngineReturnStatus::OK, post_status);\n\n    for (int i = 0; i < TEST_CHANNEL_COUNT; ++i)\n    {\n        _module_under_test->connect_audio_input_channel(i, i, empty_track_id);\n        _module_under_test->connect_audio_output_channel(i, i, empty_track_id);\n    }\n\n    // Process and verify passthrough\n    _module_under_test->process_chunk(&in_buffer, &out_buffer, &ctrl_buffer, &ctrl_buffer, Time(0), 0);\n    test_utils::assert_buffer_value(1.0f, out_buffer);\n\n    // Change the gain on the pre track and verify\n    auto track = _processors->mutable_track(\"pre\");\n    auto gain_param = track->parameter_from_name(\"gain\");\n    auto gain_event = RtEvent::make_parameter_change_event(track->id(), 0, gain_param->id(), GAIN_6DB);\n\n    track->process_event(gain_event);\n    _module_under_test->process_chunk(&in_buffer, &out_buffer, &ctrl_buffer, &ctrl_buffer, Time(0), 0);\n    EXPECT_GE(out_buffer.channel(0)[0], 1.0f);\n}\n"
  },
  {
    "path": "test/unittests/engine/event_dispatcher_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/event_dispatcher_accessor.h\"\n\n#include \"engine/event_dispatcher.cpp\"\n#include \"test_utils/engine_mockup.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::dispatcher;\n\nconstexpr float TEST_SAMPLE_RATE = 44100.0;\nconstexpr auto EVENT_PROCESS_WAIT_TIME = std::chrono::milliseconds(1);\n\nbool completed_1 = false;\nint completion_status_1 = EventStatus::NOT_HANDLED;\nbool completed_2 = false;\nint completion_status_2 = EventStatus::NOT_HANDLED;\nint last_callback = 0;\n\nvoid dummy_callback_1(void* /*arg*/, Event* /*event*/, int status)\n{\n    completed_1 = true;\n    completion_status_1 = status;\n    last_callback = 1;\n}\n\nvoid dummy_callback_2(void* /*arg*/, Event* /*event*/, int status)\n{\n    completed_2 = true;\n    completion_status_2 = status;\n    last_callback = 2;\n}\n\nint dummy_processor_callback(void* /*arg*/, EventId /*id*/)\n{\n    completed_1 = true;\n    return EventStatus::HANDLED_OK;\n}\n\nclass DummyPoster : public EventPoster\n{\npublic:\n    int process(Event* /*event*/) override\n    {\n        _received = true;\n        return EventStatus::HANDLED_OK;\n    }\n\n    bool event_received()\n    {\n        if (_received)\n        {\n            _received = false;\n            return true;\n        }\n        return false;\n    }\n\nprivate:\n    bool _received {false};\n};\n\nclass TestEventDispatcher : public ::testing::Test\n{\npublic:\n    void crank_event_loop_once()\n    {\n        _accessor->running() = false;\n        _accessor->event_loop();\n    }\n\nprotected:\n    TestEventDispatcher() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<EventDispatcher>(&_test_engine,\n                                                               &_in_rt_queue,\n                                                               &_out_rt_queue);\n\n        _accessor = std::make_unique<sushi::internal::dispatcher::Accessor>(*_module_under_test);\n    }\n\n    void TearDown() override\n    {\n        _module_under_test->stop();\n    }\n\n    std::unique_ptr<EventDispatcher> _module_under_test;\n    std::unique_ptr<sushi::internal::dispatcher::Accessor> _accessor;\n\n    EngineMockup        _test_engine {TEST_SAMPLE_RATE};\n    RtSafeRtEventFifo   _in_rt_queue;\n    RtSafeRtEventFifo   _out_rt_queue;\n    DummyPoster         _poster;\n};\n\nTEST_F(TestEventDispatcher, TestInstantiation)\n{\n    ASSERT_TRUE(_module_under_test);\n    _module_under_test->run();\n    std::this_thread::sleep_for(EVENT_PROCESS_WAIT_TIME);\n    _module_under_test->stop();\n}\n\nTEST_F(TestEventDispatcher, TestRegisteringAndDeregistering)\n{\n    auto status = _module_under_test->subscribe_to_keyboard_events(&_poster);\n    EXPECT_EQ(Status::OK, status);\n    status = _module_under_test->subscribe_to_keyboard_events(&_poster);\n    EXPECT_EQ(Status::ALREADY_SUBSCRIBED, status);\n\n    status = _module_under_test->subscribe_to_parameter_change_notifications(&_poster);\n    EXPECT_EQ(Status::OK, status);\n    status = _module_under_test->subscribe_to_parameter_change_notifications(&_poster);\n    EXPECT_EQ(Status::ALREADY_SUBSCRIBED, status);\n\n    status = _module_under_test->subscribe_to_engine_notifications(&_poster);\n    EXPECT_EQ(Status::OK, status);\n    status = _module_under_test->subscribe_to_engine_notifications(&_poster);\n    EXPECT_EQ(Status::ALREADY_SUBSCRIBED, status);\n\n    status = _module_under_test->unsubscribe_from_keyboard_events(&_poster);\n    EXPECT_EQ(Status::OK, status);\n    status = _module_under_test->unsubscribe_from_keyboard_events(&_poster);\n    EXPECT_EQ(Status::UNKNOWN_POSTER, status);\n\n    status = _module_under_test->unsubscribe_from_parameter_change_notifications(&_poster);\n    EXPECT_EQ(Status::OK, status);\n    status = _module_under_test->unsubscribe_from_parameter_change_notifications(&_poster);\n    EXPECT_EQ(Status::UNKNOWN_POSTER, status);\n\n    status = _module_under_test->unsubscribe_from_engine_notifications(&_poster);\n    EXPECT_EQ(Status::OK, status);\n    status = _module_under_test->unsubscribe_from_engine_notifications(&_poster);\n    EXPECT_EQ(Status::UNKNOWN_POSTER, status);\n}\n\nTEST_F(TestEventDispatcher, TestFromRtEventNoteOnEvent)\n{\n    RtEvent rt_event = RtEvent::make_note_on_event(10, 0, 0, 50, 10.f);\n    _in_rt_queue.push(rt_event);\n\n    _module_under_test->subscribe_to_keyboard_events(&_poster);\n    crank_event_loop_once();\n\n    ASSERT_TRUE(_poster.event_received());\n}\n\nTEST_F(TestEventDispatcher, TestFromRtEventParameterChangeNotification)\n{\n    RtEvent rt_event = RtEvent::make_parameter_change_event(10, 0, 10, 5.f);\n    _in_rt_queue.push(rt_event);\n    crank_event_loop_once();\n\n    // Just test that a parameter change was queued. More thorough testing of ParameterManager is done elsewhere\n    ASSERT_FALSE(_accessor->parameter_change_queue_empty());\n}\n\nTEST_F(TestEventDispatcher, TestEngineNotificationForwarding)\n{\n    auto event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_ADDED_TO_TRACK,\n                                                               123, 234, IMMEDIATE_PROCESS);\n    _module_under_test->post_event(std::move(event));\n\n    _module_under_test->subscribe_to_engine_notifications(&_poster);\n    crank_event_loop_once();\n\n    ASSERT_TRUE(_poster.event_received());\n}\n\nTEST_F(TestEventDispatcher, TestCompletionCallback)\n{\n    auto event = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_ADDED_TO_TRACK,\n                                                               123, 234, IMMEDIATE_PROCESS);\n    event->set_completion_cb(dummy_callback_1, nullptr);\n    completed_1 = false;\n    completion_status_1 = 0;\n\n    _module_under_test->post_event(std::move(event));\n    crank_event_loop_once();\n\n    ASSERT_TRUE(completed_1);\n    ASSERT_EQ(EventStatus::HANDLED_OK, completion_status_1);\n}\n\nTEST_F(TestEventDispatcher, TestAsyncCallbackFromProcessor)\n{\n    auto rt_event = RtEvent::make_async_work_event(dummy_processor_callback, 123, nullptr);\n    EventId sending_ev_id = rt_event.async_work_event()->event_id();\n    _in_rt_queue.push(rt_event);\n\n    /* Run the process loop once to convert from RtEvent and send the event to the worker,\n     * then run the workers process loop once to execute the event, finally run the\n     * dispatchers process loop a second time and assert that what we ended up with is\n     * an RtEvent containing a completion notification */\n    crank_event_loop_once();\n    _accessor->crank_worker();\n    crank_event_loop_once();\n\n    ASSERT_TRUE(_accessor->in_queue().empty());\n    ASSERT_FALSE(_out_rt_queue.empty());\n\n    _out_rt_queue.pop(rt_event);\n    EXPECT_EQ(RtEventType::ASYNC_WORK_NOTIFICATION, rt_event.type());\n    auto typed_event = rt_event.async_work_completion_event();\n    EXPECT_EQ(EventStatus::HANDLED_OK, typed_event->return_status());\n    EXPECT_EQ(sending_ev_id, typed_event->sending_event_id());\n    EXPECT_EQ(123u, typed_event->processor_id());\n}\n\nTEST_F(TestEventDispatcher, TestEventProcessingOrder)\n{\n    auto event_1 = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_ADDED_TO_TRACK,\n                                                                1, 1, IMMEDIATE_PROCESS);\n    event_1->set_completion_cb(dummy_callback_1, nullptr);\n    completed_1 = false;\n    completion_status_1 = EventStatus::NOT_HANDLED;\n\n    _module_under_test->post_event(std::move(event_1));\n\n    auto event_2 = std::make_unique<AudioGraphNotificationEvent>(AudioGraphNotificationEvent::Action::PROCESSOR_ADDED_TO_TRACK,\n                                                                2, 2, IMMEDIATE_PROCESS);\n    event_2->set_completion_cb(dummy_callback_2, nullptr);\n    completed_2 = false;\n    completion_status_2 = EventStatus::NOT_HANDLED;\n\n    _module_under_test->post_event(std::move(event_2));\n\n    crank_event_loop_once();\n\n    ASSERT_TRUE(completed_1);\n    ASSERT_EQ(EventStatus::HANDLED_OK, completion_status_1);\n\n    ASSERT_TRUE(completed_2);\n    ASSERT_EQ(EventStatus::HANDLED_OK, completion_status_2);\n\n    ASSERT_EQ(last_callback, 2);\n}\n\nclass TestWorker : public ::testing::Test\n{\npublic:\n    void crank_event_loop_once()\n    {\n        _accessor.running() = false;\n        _accessor.crank_worker();\n    }\n\nprotected:\n    TestWorker() = default;\n\n    void TearDown() override\n    {\n        _module_under_test.stop();\n    }\n\n    EngineMockup _test_engine {TEST_SAMPLE_RATE};\n    Worker       _module_under_test {&_test_engine, _test_engine.event_dispatcher()};\n\n    sushi::internal::dispatcher::WorkerAccessor _accessor {_module_under_test};\n};\n\nTEST_F(TestWorker, TestEventQueueingAndProcessing)\n{\n    completed_1 = false;\n    completion_status_1 = 0;\n    auto event = std::make_unique<SetEngineTempoEvent>(120.0f, IMMEDIATE_PROCESS);\n    event->set_completion_cb(dummy_callback_1, nullptr);\n    auto status = _module_under_test.dispatch(std::move(event));\n    ASSERT_EQ(EventStatus::QUEUED_HANDLING, status);\n    ASSERT_FALSE(_accessor.queue().empty());\n    crank_event_loop_once();\n    ASSERT_TRUE(completed_1);\n    ASSERT_EQ(EventStatus::HANDLED_OK, completion_status_1);\n}\n"
  },
  {
    "path": "test/unittests/engine/event_timer_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"engine/event_timer.cpp\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::event_timer;\nusing namespace std::chrono_literals;\n\nconstexpr float TEST_SAMPLE_RATE = 44000.0f;\n\nTEST(TestEventTimerInternal, TestCalcChunkTime)\n{\n    int microseconds = static_cast<int>(std::round(1000000.0f * AUDIO_CHUNK_SIZE / TEST_SAMPLE_RATE));\n    auto chunk_time = calc_chunk_time(TEST_SAMPLE_RATE);\n    ASSERT_EQ(microseconds, chunk_time.count());\n}\n\n\nclass TestEventTimer : public ::testing::Test\n{\nprotected:\n    TestEventTimer() = default;\n\n    EventTimer _module_under_test {TEST_SAMPLE_RATE};\n};\n\nTEST_F(TestEventTimer, TestToOffsetConversion)\n{\n    _module_under_test.set_incoming_time(1s);\n    bool send_now;\n    int offset;\n\n    /* A timestamp far into the future should return false */\n    std::tie(send_now, offset) = _module_under_test.sample_offset_from_realtime(3s);\n    ASSERT_FALSE(send_now);\n\n    /* A timestamp in the past should return true and offset 0 */\n    std::tie(send_now, offset) = _module_under_test.sample_offset_from_realtime(0s);\n    ASSERT_TRUE(send_now);\n    ASSERT_EQ(0, offset);\n\n    /* Create a timestamp in the middle of the chunk, note we must add\n     * chunk time here because the EventTimer is 1 chunk ahead internally,\n     * Because of rounding errors, the resulting sample offset could be\n     * both AUDIO_CHUNK_SIZE / 2 and AUDIO_CHUNK_SIZE / 2 -1 */\n    auto chunk_time = calc_chunk_time(TEST_SAMPLE_RATE);\n    Time timestamp = 1s + chunk_time + chunk_time / 2;\n    std::tie(send_now, offset) = _module_under_test.sample_offset_from_realtime(timestamp);\n    ASSERT_TRUE(send_now);\n    ASSERT_GE(offset, AUDIO_CHUNK_SIZE / 2 - 1);\n    ASSERT_LE(offset, AUDIO_CHUNK_SIZE / 2);\n}\n\nTEST_F(TestEventTimer, TestToRealTimesConversion)\n{\n    auto chunk_time = calc_chunk_time(TEST_SAMPLE_RATE);\n    _module_under_test.set_outgoing_time(1s);\n\n    Time timestamp = _module_under_test.real_time_from_sample_offset(0);\n    ASSERT_EQ((1s + chunk_time).count(), timestamp.count());\n\n    timestamp = _module_under_test.real_time_from_sample_offset(AUDIO_CHUNK_SIZE / 2);\n    ASSERT_EQ((1s + chunk_time + chunk_time / 2).count(), timestamp.count());\n}"
  },
  {
    "path": "test/unittests/engine/factories/factories_test.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n#include <gmock/gmock.h>\n#include <gmock/gmock-actions.h>\n\n#include \"sushi/offline_factory.h\"\n#include \"sushi/standalone_factory.h\"\n\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/portaudio_mockup.h\"\n\nconstexpr int MOCK_CHANNEL_COUNT = 10;\n\n#ifndef SUSHI_BUILD_WITH_PORTAUDIO\n\n#include \"audio_frontends/portaudio_frontend.h\"\n\n// Needed for mocking the frontend.\nnamespace sushi::internal::audio_frontend {\n\nPortAudioFrontend::PortAudioFrontend(engine::BaseEngine* engine) : BaseAudioFrontend(engine)\n{\n    _engine->set_audio_channels(MOCK_CHANNEL_COUNT, MOCK_CHANNEL_COUNT);\n}\n\nAudioFrontendStatus PortAudioFrontend::init(BaseAudioFrontendConfiguration*)\n{\n    return AudioFrontendStatus::OK;\n}\n\n}\n#endif\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#ifndef OSCPACK_UNIT_TESTS\n#include \"third-party/oscpack/osc/OscPacketListener.h\"\n#include \"third-party/oscpack/ip/UdpSocket.h\"\n#endif\n\n#include \"concrete_sushi.cpp\"\n#include \"sushi/terminal_utilities.h\"\n#include \"factories/base_factory.cpp\"\n#include \"factories/offline_factory.cpp\"\n#include \"factories/offline_factory_implementation.cpp\"\n#include \"factories/reactive_factory.cpp\"\n#include \"factories/reactive_factory_implementation.cpp\"\n#include \"factories/standalone_factory.cpp\"\n#include \"factories/standalone_factory_implementation.cpp\"\n\n#ifndef SUSHI_BUILD_WITH_APPLE_COREAUDIO\n#include \"audio_frontends/apple_coreaudio_frontend.cpp\"\n#endif\n\nusing namespace std::chrono_literals;\n\nusing ::testing::Return;\nusing ::testing::NiceMock;\nusing ::testing::_;\n\nnamespace sushi::internal\n{\n\nTEST (TestTerminalUtils, TestStringTokenizer)\n{\n    auto args = sushi::tokenize_arg(\"one:two:three\", ':');\n    GTEST_ASSERT_EQ(3, args.size());\n    GTEST_ASSERT_EQ(\"one\", args[0]);\n    GTEST_ASSERT_EQ(\"two\", args[1]);\n    GTEST_ASSERT_EQ(\"three\", args[2]);\n\n    args = sushi::tokenize_arg(\"single\", ',');\n    GTEST_ASSERT_EQ(1, args.size());\n    GTEST_ASSERT_EQ(\"single\", args.front());\n\n    args = sushi::tokenize_arg(\":one:two::\", ':');\n    GTEST_ASSERT_EQ(5, args.size());\n    GTEST_ASSERT_EQ(\"\", args[0]);\n    GTEST_ASSERT_EQ(\"one\", args[1]);\n    GTEST_ASSERT_EQ(\"two\", args[2]);\n    GTEST_ASSERT_EQ(\"\", args[3]);\n    GTEST_ASSERT_EQ(\"\", args[4]);\n}\n\nclass ConcreteSushiAccessor\n{\npublic:\n    explicit ConcreteSushiAccessor(ConcreteSushi& f) : _friend(f) {}\n\n    const std::unique_ptr<engine::AudioEngine>& engine()\n    {\n        return _friend._engine;\n    }\n\n    const std::unique_ptr<midi_dispatcher::MidiDispatcher>& midi_dispatcher()\n    {\n        return _friend._midi_dispatcher;\n    }\n\n    const std::unique_ptr<midi_frontend::BaseMidiFrontend>& midi_frontend()\n    {\n        return _friend._midi_frontend;\n    }\n\n    const std::unique_ptr<control_frontend::OSCFrontend>& osc_frontend()\n    {\n        return _friend._osc_frontend;\n    }\n\n    const std::unique_ptr<audio_frontend::BaseAudioFrontend>& audio_frontend()\n    {\n        return _friend._audio_frontend;\n    }\n\n    const std::unique_ptr<audio_frontend::BaseAudioFrontendConfiguration>& frontend_config()\n    {\n        return _friend._frontend_config;\n    }\n\n    const std::unique_ptr<engine::Controller>& engine_controller()\n    {\n        return _friend._engine_controller;\n    }\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n    const std::unique_ptr<sushi_rpc::GrpcServer>& rpc_server()\n    {\n        return _friend._rpc_server;\n    }\n#endif\n\nprivate:\n    ConcreteSushi& _friend;\n};\n\n} // end namespace sushi::internal\n\nusing namespace sushi;\nusing namespace sushi::internal;\n\n//////////////////////////////////////////////////////\n// ReactiveFactory\n//////////////////////////////////////////////////////\n\nclass ReactiveFactoryTest : public ::testing::Test\n{\nprotected:\n    ReactiveFactoryTest() = default;\n\n    void SetUp() override\n    {\n        options.config_filename = \"NONE\";\n        options.config_source = ConfigurationSource::NONE;\n\n        _path = test_utils::get_data_dir_path();\n    }\n\n    SushiOptions options;\n\n    ReactiveFactoryImplementation _reactive_factory;\n\n    std::string _path;\n};\n\nTEST_F(ReactiveFactoryTest, TestReactiveFactoryWithDefaultConfig)\n{\n    options.config_filename = \"NONE\";\n    options.config_source = ConfigurationSource::NONE;\n\n    auto [sushi, status] = _reactive_factory.new_instance(options);\n\n    ASSERT_NE(sushi.get(), nullptr);\n\n    auto sushi_cast = static_cast<ConcreteSushi*>(sushi.get());\n\n    ASSERT_NE(sushi_cast, nullptr);\n\n    sushi::internal::ConcreteSushiAccessor _accessor(*sushi_cast);\n\n    EXPECT_NE(_accessor.osc_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.engine().get(), nullptr);\n    EXPECT_NE(_accessor.midi_dispatcher().get(), nullptr);\n    EXPECT_NE(_accessor.midi_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.audio_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.frontend_config().get(), nullptr);\n    EXPECT_NE(_accessor.engine_controller().get(), nullptr);\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n    EXPECT_NE(sushi_cast->_rpc_server.get(), nullptr);\n#endif\n\n    auto rt_controller = _reactive_factory.rt_controller();\n\n    EXPECT_NE(rt_controller.get(), nullptr);\n}\n\nTEST_F(ReactiveFactoryTest, TestPassiveFactoryWithConfigFile)\n{\n    // Currently, the Passive frontend supports only stereo I/O, so a simpler config is used.\n    // JsonConfigurator is already extensively tested elsewhere anyway.\n    _path.append(\"config_single_stereo.json\");\n\n    options.config_filename = _path;\n    options.config_source = ConfigurationSource::FILE;\n\n    auto [sushi, status] = _reactive_factory.new_instance(options);\n\n    ASSERT_NE(sushi.get(), nullptr);\n\n    auto sushi_cast = static_cast<ConcreteSushi*>(sushi.get());\n\n    ASSERT_NE(sushi_cast, nullptr);\n\n    sushi::internal::ConcreteSushiAccessor _accessor(*sushi_cast);\n\n    EXPECT_NE(_accessor.osc_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.engine().get(), nullptr);\n    EXPECT_NE(_accessor.midi_dispatcher().get(), nullptr);\n    EXPECT_NE(_accessor.midi_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.audio_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.frontend_config().get(), nullptr);\n    EXPECT_NE(_accessor.engine_controller().get(), nullptr);\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n    EXPECT_NE(sushi_cast->_rpc_server.get(), nullptr);\n#endif\n\n    auto rt_controller = _reactive_factory.rt_controller();\n\n    EXPECT_NE(rt_controller.get(), nullptr);\n}\n\n//////////////////////////////////////////////////////\n// OfflineFactory\n//////////////////////////////////////////////////////\n\nclass OfflineFactoryTest : public ::testing::Test\n{\nprotected:\n    OfflineFactoryTest() = default;\n\n    void SetUp() override\n    {\n        options.config_filename = \"NONE\";\n        options.config_source = ConfigurationSource::NONE;\n\n        _path = test_utils::get_data_dir_path();\n    }\n\n    SushiOptions options;\n\n    OfflineFactory _offline_factory;\n\n    std::string _path;\n};\n\nTEST_F(OfflineFactoryTest, TestOfflineFactoryWithDefaultConfig)\n{\n    options.config_filename = \"NONE\";\n    options.config_source = ConfigurationSource::NONE;\n\n    auto [sushi, status] = _offline_factory.new_instance(options);\n\n    ASSERT_NE(sushi.get(), nullptr);\n\n    auto sushi_cast = static_cast<ConcreteSushi*>(sushi.get());\n\n    ASSERT_NE(sushi_cast, nullptr);\n\n    sushi::internal::ConcreteSushiAccessor _accessor(*sushi_cast);\n\n    // OSC Frontend instantiation will not be implemented for the offline factory.\n    EXPECT_EQ(_accessor.osc_frontend().get(), nullptr);\n\n    EXPECT_NE(_accessor.engine().get(), nullptr);\n    EXPECT_NE(_accessor.midi_dispatcher().get(), nullptr);\n    EXPECT_NE(_accessor.midi_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.audio_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.frontend_config().get(), nullptr);\n    EXPECT_NE(_accessor.engine_controller().get(), nullptr);\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n    EXPECT_NE(sushi_cast->_rpc_server.get(), nullptr);\n#endif\n}\n\nTEST_F(OfflineFactoryTest, TestOfflineFactoryWithConfigFile)\n{\n    _path.append(\"config.json\");\n\n    options.config_filename = _path;\n    options.config_source = ConfigurationSource::FILE;\n\n    auto [sushi, status] = _offline_factory.new_instance(options);\n\n    ASSERT_NE(sushi.get(), nullptr);\n\n    auto sushi_cast = static_cast<ConcreteSushi*>(sushi.get());\n\n    ASSERT_NE(sushi_cast, nullptr);\n\n    sushi::internal::ConcreteSushiAccessor _accessor(*sushi_cast);\n\n    // OSC Frontend instantiation will not be implemented for the offline factory.\n    EXPECT_EQ(_accessor.osc_frontend().get(), nullptr);\n\n    EXPECT_NE(_accessor.engine().get(), nullptr);\n    EXPECT_NE(_accessor.midi_dispatcher().get(), nullptr);\n    EXPECT_NE(_accessor.midi_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.audio_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.frontend_config().get(), nullptr);\n    EXPECT_NE(_accessor.engine_controller().get(), nullptr);\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n    EXPECT_NE(sushi_cast->_rpc_server.get(), nullptr);\n#endif\n}\n\n\n//////////////////////////////////////////////////////\n// StandaloneFactory\n//////////////////////////////////////////////////////\n\nclass StandaloneFactoryTest : public ::testing::Test\n{\nprotected:\n    StandaloneFactoryTest() = default;\n\n    void SetUp() override\n    {\n        mockPortAudio = std::make_unique<NiceMock<MockPortAudio>>();\n\n        PaError init_value = PaErrorCode::paNoError;\n        EXPECT_CALL(*mockPortAudio, Pa_Initialize).WillRepeatedly(Return(init_value));\n        EXPECT_CALL(*mockPortAudio, Pa_GetDeviceCount).WillRepeatedly(Return(1));\n        EXPECT_CALL(*mockPortAudio, Pa_GetDeviceInfo).WillRepeatedly(Return(&device_info));\n        EXPECT_CALL(*mockPortAudio, Pa_GetStreamInfo).WillRepeatedly(Return(&stream_info));\n        EXPECT_CALL(*mockPortAudio, Pa_OpenStream).WillRepeatedly(Return(init_value));\n\n        options.config_filename = \"NONE\";\n        options.config_source = ConfigurationSource::NONE;\n\n        device_info.maxInputChannels = MOCK_CHANNEL_COUNT;\n        device_info.maxOutputChannels = MOCK_CHANNEL_COUNT;\n\n        _path = test_utils::get_data_dir_path();\n    }\n\n    void TearDown() override\n    {\n        mockPortAudio.reset();\n    }\n\n    PaDeviceInfo device_info;\n    PaStreamInfo stream_info;\n\n    SushiOptions options;\n\n    StandaloneFactory _standalone_factory;\n\n    std::string _path;\n};\n\nTEST_F(StandaloneFactoryTest, TestStandaloneFactoryWithDefaultConfig)\n{\n    auto expected_name = \"a_device\";\n\n#ifdef SUSHI_BUILD_WITH_PORTAUDIO\n    PaDeviceInfo device_info;\n    device_info.maxInputChannels = 1;\n    device_info.maxOutputChannels = 1;\n    device_info.name = expected_name;\n\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceInfo)\n#ifdef __APPLE__\n    .WillOnce(Return(&device_info))\n#endif\n    .WillOnce(Return(&device_info))\n    .WillOnce(Return(&device_info));\n#endif\n\n    options.config_filename = \"NONE\";\n    options.config_source = ConfigurationSource::NONE;\n    options.frontend_type = FrontendType::PORTAUDIO;\n    options.device_name = expected_name;\n\n    auto [sushi, status] = _standalone_factory.new_instance(options);\n\n    ASSERT_NE(sushi.get(), nullptr);\n\n    auto sushi_cast = static_cast<ConcreteSushi*>(sushi.get());\n\n    ASSERT_NE(sushi_cast, nullptr);\n\n    sushi::internal::ConcreteSushiAccessor _accessor(*sushi_cast);\n\n    EXPECT_NE(_accessor.osc_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.engine().get(), nullptr);\n    EXPECT_NE(_accessor.midi_dispatcher().get(), nullptr);\n    EXPECT_NE(_accessor.midi_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.audio_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.frontend_config().get(), nullptr);\n    EXPECT_NE(_accessor.engine_controller().get(), nullptr);\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n    EXPECT_NE(sushi_cast->_rpc_server.get(), nullptr);\n#endif\n}\n\nTEST_F(StandaloneFactoryTest, TestStandaloneFactoryWithConfigFile)\n{\n    auto expected_name = \"a_device\";\n\n#ifdef SUSHI_BUILD_WITH_PORTAUDIO\n    PaDeviceInfo device_info;\n    device_info.maxInputChannels = 2;\n    device_info.maxOutputChannels = 2;\n    device_info.name = expected_name;\n\n    EXPECT_CALL(*mockPortAudio, Pa_GetDeviceInfo)\n#ifdef __APPLE__\n    .WillOnce(Return(&device_info))\n#endif\n    .WillOnce(Return(&device_info))\n    .WillOnce(Return(&device_info));\n#endif\n\n    _path.append(\"config_single_stereo.json\");\n\n    options.config_filename = _path;\n    options.config_source = ConfigurationSource::FILE;\n    options.frontend_type = FrontendType::PORTAUDIO;\n    options.device_name = expected_name;\n\n    auto [sushi, status] = _standalone_factory.new_instance(options);\n\n    ASSERT_NE(sushi.get(), nullptr);\n\n    auto sushi_cast = static_cast<ConcreteSushi*>(sushi.get());\n\n    ASSERT_NE(sushi_cast, nullptr);\n\n    sushi::internal::ConcreteSushiAccessor _accessor(*sushi_cast);\n\n    EXPECT_NE(_accessor.osc_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.engine().get(), nullptr);\n    EXPECT_NE(_accessor.midi_dispatcher().get(), nullptr);\n    EXPECT_NE(_accessor.midi_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.audio_frontend().get(), nullptr);\n    EXPECT_NE(_accessor.frontend_config().get(), nullptr);\n    EXPECT_NE(_accessor.engine_controller().get(), nullptr);\n\n#ifdef SUSHI_BUILD_WITH_RPC_INTERFACE\n    EXPECT_NE(sushi_cast->_rpc_server.get(), nullptr);\n#endif\n}\n"
  },
  {
    "path": "test/unittests/engine/json_configurator_test.cpp",
    "content": "#include <fstream>\n\n#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include <gmock/gmock.h>\n#include <gmock/gmock-actions.h>\n\n#include \"engine/json_configurator.cpp\"\n#include \"sushi/utils.h\"\n\n#include \"engine/audio_engine.h\"\n#include \"engine/midi_dispatcher.h\"\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/control_mockup.h\"\n\n#include \"test_utils/mock_osc_interface.h\"\n\nnamespace sushi::internal::midi_dispatcher\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(MidiDispatcher& f) : _friend(f) {}\n\n    [[nodiscard]] const MidiDispatcher::KeyboardRoutesIn& kb_routes_in()\n    {\n        return _friend._kb_routes_in;\n    }\n\n    [[nodiscard]] const MidiDispatcher::CcRoutes& cc_routes()\n    {\n        return _friend._cc_routes;\n    }\n\n    [[nodiscard]] const MidiDispatcher::RawRoutesIn& raw_routes_in()\n    {\n        return _friend._raw_routes_in;\n    }\n\n    [[nodiscard]] const MidiDispatcher::PcRoutes& pc_routes()\n    {\n        return _friend._pc_routes;\n    }\n\nprivate:\n    MidiDispatcher& _friend;\n};\n\n}\n\nnamespace sushi::internal::jsonconfig\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(JsonConfigurator& f) : _friend(f) {}\n\n    [[nodiscard]] JsonConfigReturnStatus make_track(const rapidjson::Value& track_def, engine::TrackType type)\n    {\n        return _friend._make_track(track_def, type);\n    }\n\n    static bool validate_against_schema(rapidjson::Value& config, JsonSection section)\n    {\n        return sushi::internal::jsonconfig::JsonConfigurator::_validate_against_schema(config, section);\n    }\n\n    std::pair<JsonConfigReturnStatus, const rapidjson::Value&> parse_section(JsonSection section)\n    {\n        return _friend._parse_section(section);\n    }\n\nprivate:\n    JsonConfigurator& _friend;\n};\n\n}\n\nusing ::testing::Return;\nusing ::testing::NiceMock;\nusing ::testing::_;\n\nconstexpr unsigned int SAMPLE_RATE = 44000;\nconstexpr unsigned int ENGINE_CHANNELS = 8;\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\nusing namespace sushi::internal::jsonconfig;\nusing namespace sushi::internal::control_frontend;\n\nclass TestJsonConfigurator : public ::testing::Test\n{\nprotected:\n    TestJsonConfigurator() = default;\n\n    void SetUp() override\n    {\n        _engine.set_audio_channels(ENGINE_CHANNELS, ENGINE_CHANNELS);\n        _path = test_utils::get_data_dir_path();\n        _path.append(\"config.json\");\n\n        auto json_data = sushi::read_file(_path);\n\n        _module_under_test = std::make_unique<JsonConfigurator>(&_engine,\n                                                                &_midi_dispatcher,\n                                                                _engine.processor_container(),\n                                                                json_data.value());\n\n        _accessor = std::make_unique<sushi::internal::jsonconfig::Accessor>(*_module_under_test);\n    }\n\n    /* Helper functions */\n    JsonConfigReturnStatus _make_track(const rapidjson::Value &track, TrackType type);\n\n    AudioEngine _engine {SAMPLE_RATE};\n    MidiDispatcher _midi_dispatcher {_engine.event_dispatcher()};\n\n    sushi::internal::midi_dispatcher::Accessor _midi_dispatcher_accessor {_midi_dispatcher};\n\n    sushi::control::ControlMockup _controller;\n\n    std::unique_ptr<JsonConfigurator> _module_under_test;\n\n    std::unique_ptr<sushi::internal::jsonconfig::Accessor> _accessor;\n\n    std::string _path;\n};\n\nJsonConfigReturnStatus TestJsonConfigurator::_make_track(const rapidjson::Value &track, TrackType type)\n{\n    return _accessor->make_track(track, type);\n}\n\nTEST_F(TestJsonConfigurator, TestLoadAudioConfig)\n{\n    auto [status, control_config] = _module_under_test->load_control_config();\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n    ASSERT_TRUE(control_config.cv_inputs.has_value());\n    ASSERT_EQ(1, control_config.cv_inputs.value());\n    ASSERT_TRUE(control_config.cv_outputs.has_value());\n    ASSERT_EQ(2, control_config.cv_outputs.value());\n}\n\nTEST_F(TestJsonConfigurator, TestLoadHostConfig)\n{\n    auto status = _module_under_test->load_host_config();\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n    ASSERT_FLOAT_EQ(48000.0f, _engine.sample_rate());\n}\n\nTEST_F(TestJsonConfigurator, TestLoadTracks)\n{\n    auto status = _module_under_test->load_tracks();\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n    auto tracks = _engine.processor_container()->all_tracks();\n\n    ASSERT_EQ(5u, tracks.size());\n    auto track_1_processors = _engine.processor_container()->processors_on_track(tracks[0]->id());\n    auto track_2_processors = _engine.processor_container()->processors_on_track(tracks[1]->id());\n\n    ASSERT_EQ(3u, track_1_processors.size());\n    ASSERT_EQ(3u, track_2_processors.size());\n\n    ASSERT_EQ(\"passthrough_0_l\", track_1_processors[0]->name());\n    ASSERT_EQ(\"gain_0_l\", track_1_processors[1]->name());\n    ASSERT_EQ(\"equalizer_0_l\", track_1_processors[2]->name());\n\n    ASSERT_EQ(\"gain_0_r\", track_2_processors[0]->name());\n    ASSERT_EQ(\"passthrough_0_r\", track_2_processors[1]->name());\n    ASSERT_EQ(\"gain_1_r\", track_2_processors[2]->name());\n}\n\nTEST_F(TestJsonConfigurator, TestLoadMidi)\n{\n    auto status = _module_under_test->load_tracks();\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n    _midi_dispatcher.set_midi_inputs(1);\n    _midi_dispatcher.set_midi_outputs(1);\n\n    status = _module_under_test->load_midi();\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n    ASSERT_EQ(1u, _midi_dispatcher_accessor.kb_routes_in().size());\n    ASSERT_EQ(1u, _midi_dispatcher_accessor.cc_routes().size());\n    ASSERT_EQ(1u, _midi_dispatcher_accessor.raw_routes_in().size());\n    ASSERT_EQ(1u, _midi_dispatcher_accessor.pc_routes().size());\n    ASSERT_TRUE(_midi_dispatcher.midi_clock_enabled(0));\n}\n\nTEST_F(TestJsonConfigurator, TestLoadOsc)\n{\n    // osc_frontend is only used in this test, so no need to keep in harness.\n    constexpr int OSC_TEST_SERVER_PORT = 24024;\n    constexpr int OSC_TEST_SEND_PORT = 24023;\n    constexpr auto OSC_TEST_SEND_ADDRESS = \"127.0.0.1\";\n\n    auto osc_interface = new NiceMock<MockOscInterface>(OSC_TEST_SERVER_PORT,\n                                                        OSC_TEST_SEND_PORT,\n                                                        OSC_TEST_SEND_ADDRESS);\n\n    OSCFrontend osc_frontend{&_engine, &_controller, osc_interface};\n\n    _module_under_test->set_osc_frontend(&osc_frontend);\n\n    EXPECT_CALL(*osc_interface, init()).Times(1).WillOnce(Return(true));\n\n    ASSERT_EQ(ControlFrontendStatus::OK, osc_frontend.init());\n\n    auto status = _module_under_test->load_tracks();\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n\n    auto outputs_before = osc_frontend.get_enabled_parameter_outputs();\n\n    ASSERT_EQ(0u, outputs_before.size());\n\n    status = _module_under_test->load_osc();\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n\n    auto outputs_after = osc_frontend.get_enabled_parameter_outputs();\n\n    ASSERT_EQ(1u, outputs_after.size());\n}\n\nTEST_F(TestJsonConfigurator, TestLoadCvGateControl)\n{\n    auto status = _module_under_test->load_tracks();\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n\n    status = _module_under_test->load_cv_gate();\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n}\n\nTEST_F(TestJsonConfigurator, TestLoadInitialState)\n{\n    auto status = _module_under_test->load_tracks();\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n\n    status = _module_under_test->load_initial_state();\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n\n    auto main_instance = _engine.processor_container()->mutable_processor(\"main\");\n    ASSERT_TRUE(main_instance.get());\n    auto pan_info = main_instance->parameter_from_name(\"pan\");\n    ASSERT_TRUE(pan_info);\n    ASSERT_FLOAT_EQ(0.35f, main_instance->parameter_value(pan_info->id()).second);\n}\n\nTEST_F(TestJsonConfigurator, TestMakeChain)\n{\n    /* Create plugin track without processors */\n    rapidjson::Document test_cfg;\n    rapidjson::Value track(rapidjson::kObjectType);\n    rapidjson::Value channels(1);\n    rapidjson::Value name(\"track_without_plugins\");\n    rapidjson::Value inputs(rapidjson::kArrayType);\n    rapidjson::Value outputs(rapidjson::kArrayType);\n    rapidjson::Value plugins(rapidjson::kArrayType);\n    track.AddMember(\"channels\", channels, test_cfg.GetAllocator());\n    track.AddMember(\"name\", name, test_cfg.GetAllocator());\n    track.AddMember(\"inputs\", inputs, test_cfg.GetAllocator());\n    track.AddMember(\"outputs\", outputs, test_cfg.GetAllocator());\n    track.AddMember(\"plugins\", plugins, test_cfg.GetAllocator());\n    ASSERT_EQ(_make_track(track, TrackType::REGULAR), JsonConfigReturnStatus::OK);\n\n    /* Similar Plugin track but with same track id */\n    track[\"channels\"] = 2;\n    ASSERT_EQ(_make_track(track, TrackType::REGULAR), JsonConfigReturnStatus::INVALID_TRACK_NAME);\n\n    /* Create valid plugin track with valid plugin */\n    track[\"name\"] = \"tracks_with_internal_plugin\";\n    rapidjson::Value test_plugin(rapidjson::kObjectType);\n    rapidjson::Value uid(\"sushi.testing.gain\");\n    rapidjson::Value path(\"empty_path\");\n    rapidjson::Value type(\"internal\");\n    rapidjson::Value plugin_name(\"internal_plugin\");\n    test_plugin.AddMember(\"uid\", uid, test_cfg.GetAllocator());\n    test_plugin.AddMember(\"path\", path, test_cfg.GetAllocator());\n    test_plugin.AddMember(\"type\", type, test_cfg.GetAllocator());\n    test_plugin.AddMember(\"name\", plugin_name, test_cfg.GetAllocator());\n    track[\"plugins\"].PushBack(test_plugin, test_cfg.GetAllocator());\n    ASSERT_EQ(_make_track(track, TrackType::REGULAR), JsonConfigReturnStatus::OK);\n\n    rapidjson::Value& plugin = track[\"plugins\"][0];\n    track[\"name\"] = \"track_invalid_internal\";\n    plugin[\"name\"] = \"invalid_internal_plugin\";\n    plugin[\"uid\"] = \"wrong_uid\";\n    plugin[\"type\"] = \"internal\";\n    ASSERT_EQ(_make_track(track, TrackType::REGULAR), JsonConfigReturnStatus::INVALID_CONFIGURATION);\n\n    track[\"name\"] = \"track_invalid_name\";\n    plugin[\"name\"] = \"internal_plugin\";\n    plugin[\"uid\"] = \"sushi.testing.gain\";\n    plugin[\"type\"] = \"internal\";\n    ASSERT_EQ(_make_track(track, TrackType::REGULAR), JsonConfigReturnStatus::INVALID_CONFIGURATION);\n}\n\nTEST_F(TestJsonConfigurator, TestValidJsonSchema)\n{\n    std::ifstream config_file(_path);\n    std::string config_file_contents((std::istreambuf_iterator<char>(config_file)), std::istreambuf_iterator<char>());\n    rapidjson::Document test_cfg;\n    test_cfg.Parse(config_file_contents.c_str());\n    ASSERT_TRUE(_accessor->validate_against_schema(test_cfg,JsonSection::HOST_CONFIG));\n    ASSERT_TRUE(_accessor->validate_against_schema(test_cfg,JsonSection::TRACKS));\n    ASSERT_TRUE(_accessor->validate_against_schema(test_cfg,JsonSection::MIDI));\n    ASSERT_TRUE(_accessor->validate_against_schema(test_cfg,JsonSection::CV_GATE));\n    ASSERT_TRUE(_accessor->validate_against_schema(test_cfg,JsonSection::EVENTS));\n    ASSERT_TRUE(_accessor->validate_against_schema(test_cfg,JsonSection::STATE));\n}\n\nTEST_F(TestJsonConfigurator, TestHostConfigSchema)\n{\n    rapidjson::Document test_cfg;\n    test_cfg.SetObject();\n    /* No definition of host_config */\n    ASSERT_FALSE(_accessor->validate_against_schema(test_cfg, JsonSection::HOST_CONFIG));\n\n    /* no definition of samplerate */\n    rapidjson::Value host_config(rapidjson::kObjectType);\n    test_cfg.AddMember(\"host_config\", host_config, test_cfg.GetAllocator());\n    ASSERT_FALSE(_accessor->validate_against_schema(test_cfg, JsonSection::HOST_CONFIG));\n\n    /* invalid type */\n    rapidjson::Value samplerate(rapidjson::kObjectType);\n    test_cfg[\"host_config\"].AddMember(\"samplerate\", samplerate, test_cfg.GetAllocator());\n    test_cfg[\"host_config\"][\"samplerate\"] = \"44100\";\n    ASSERT_FALSE(_accessor->validate_against_schema(test_cfg, JsonSection::HOST_CONFIG));\n}\n\nTEST_F(TestJsonConfigurator, TestPluginChainSchema)\n{\n    rapidjson::Document test_cfg;\n    test_cfg.SetObject();\n\n    rapidjson::Value tracks(rapidjson::kArrayType);\n    test_cfg.AddMember(\"tracks\", tracks, test_cfg.GetAllocator());\n\n    /* Plugin track without plugin list defined is not ok, empty list defined is ok */\n    rapidjson::Value example_track(rapidjson::kObjectType);\n    rapidjson::Value channels(1);\n    rapidjson::Value name(\"track_name\");\n    rapidjson::Value inputs(rapidjson::kArrayType);\n    rapidjson::Value outputs(rapidjson::kArrayType);\n    rapidjson::Value plugins(rapidjson::kArrayType);\n    example_track.AddMember(\"channels\", channels, test_cfg.GetAllocator());\n    example_track.AddMember(\"name\", name, test_cfg.GetAllocator());\n    example_track.AddMember(\"inputs\", inputs, test_cfg.GetAllocator());\n    example_track.AddMember(\"outputs\", outputs, test_cfg.GetAllocator());\n    test_cfg[\"tracks\"].PushBack(example_track, test_cfg.GetAllocator());\n    ASSERT_FALSE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n    test_cfg[\"tracks\"][0].AddMember(\"plugins\", plugins, test_cfg.GetAllocator());\n    ASSERT_TRUE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n\n    /* incorrect mode */\n    test_cfg[\"tracks\"][0][\"channels\"] = -1;\n    ASSERT_FALSE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n    test_cfg[\"tracks\"][0][\"channels\"] = 2;\n    ASSERT_TRUE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n}\n\nTEST_F(TestJsonConfigurator, TestPluginSchema)\n{\n    rapidjson::Document test_cfg;\n    test_cfg.SetObject();\n    rapidjson::Value tracks(rapidjson::kArrayType);\n    test_cfg.AddMember(\"tracks\", tracks, test_cfg.GetAllocator());\n\n    rapidjson::Value example_track(rapidjson::kObjectType);\n    rapidjson::Value track_name(\"track_name\");\n    rapidjson::Value channels(1);\n    rapidjson::Value inputs(rapidjson::kArrayType);\n    rapidjson::Value outputs(rapidjson::kArrayType);\n    rapidjson::Value plugins(rapidjson::kArrayType);\n    example_track.AddMember(\"name\", track_name, test_cfg.GetAllocator());\n    example_track.AddMember(\"channels\", channels, test_cfg.GetAllocator());\n    example_track.AddMember(\"inputs\", inputs, test_cfg.GetAllocator());\n    example_track.AddMember(\"outputs\", outputs, test_cfg.GetAllocator());\n    example_track.AddMember(\"plugins\", plugins, test_cfg.GetAllocator());\n    test_cfg[\"tracks\"].PushBack(example_track, test_cfg.GetAllocator());\n\n    rapidjson::Value example_plugin(rapidjson::kObjectType);\n    rapidjson::Value plugin_name(\"plugin_name\");\n    rapidjson::Value path(\"plugin_path\");\n    rapidjson::Value uid(\"plugin_name\");\n    rapidjson::Value type(\"internal\");\n    example_plugin.AddMember(\"name\", plugin_name, test_cfg.GetAllocator());\n    example_plugin.AddMember(\"type\", type, test_cfg.GetAllocator());\n    test_cfg[\"tracks\"][0][\"plugins\"].PushBack(example_plugin, test_cfg.GetAllocator());\n    rapidjson::Value& plugin = test_cfg[\"tracks\"][0][\"plugins\"][0];\n\n    /* type = internal; requires uid */\n    ASSERT_FALSE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n    plugin.AddMember(\"uid\", uid, test_cfg.GetAllocator());\n    ASSERT_TRUE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n    plugin[\"type\"] = \"vst3x\";\n    ASSERT_FALSE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n\n    /* type = vst2x; requires path */\n    plugin[\"type\"] = \"vst2x\";\n    ASSERT_FALSE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n    plugin.AddMember(\"path\", path, test_cfg.GetAllocator());\n    plugin.RemoveMember(\"uid\");\n    ASSERT_TRUE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n    plugin[\"type\"] = \"vst3x\";\n    ASSERT_FALSE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n\n    /* type = vst3x; requires uid & path */\n    rapidjson::Value vst3_uid(\"vst3_uid\");\n    plugin.AddMember(\"uid\", vst3_uid, test_cfg.GetAllocator());\n    ASSERT_TRUE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n\n    /* type = LV2; requires name & uri */\n    plugin[\"type\"] = \"lv2\";\n    ASSERT_FALSE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n    plugin.AddMember(\"uri\", path, test_cfg.GetAllocator());\n    plugin.RemoveMember(\"uid\");\n    ASSERT_FALSE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n    plugin[\"type\"] = \"vst3x\";\n    ASSERT_FALSE(_accessor->validate_against_schema(test_cfg, JsonSection::TRACKS));\n}\n\nTEST_F(TestJsonConfigurator, TestMidiSchema)\n{\n    auto [status, midi_cfg] = _accessor->parse_section(JsonSection::MIDI);\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n\n    rapidjson::Document mutable_cfg;\n    mutable_cfg.SetObject();\n    rapidjson::Value val(rapidjson::kObjectType);\n    mutable_cfg.AddMember(\"midi\", val, mutable_cfg.GetAllocator());\n    mutable_cfg[\"midi\"].CopyFrom(midi_cfg, mutable_cfg.GetAllocator());\n\n    rapidjson::Value& track_connections = mutable_cfg[\"midi\"][\"track_connections\"][0];\n    ASSERT_TRUE(_accessor->validate_against_schema(mutable_cfg, JsonSection::MIDI));\n    track_connections[\"channel\"] = \"invalid\";\n    ASSERT_FALSE(_accessor->validate_against_schema(mutable_cfg, JsonSection::MIDI));\n    track_connections[\"channel\"] = 16;\n    ASSERT_FALSE(_accessor->validate_against_schema(mutable_cfg, JsonSection::MIDI));\n}\n\nTEST_F(TestJsonConfigurator, TestCvGateSchema)\n{\n    auto [status, test_cfg] = _accessor->parse_section(JsonSection::CV_GATE);\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n\n    rapidjson::Document mutable_cfg;\n    mutable_cfg.SetObject();\n    rapidjson::Value val(rapidjson::kObjectType);\n    mutable_cfg.AddMember(\"cv_control\", val, mutable_cfg.GetAllocator());\n    mutable_cfg[\"cv_control\"].CopyFrom(test_cfg, mutable_cfg.GetAllocator());\n\n    rapidjson::Value& cv_in = mutable_cfg[\"cv_control\"][\"cv_inputs\"][0];\n    ASSERT_TRUE(_accessor->validate_against_schema(mutable_cfg, JsonSection::CV_GATE));\n    cv_in[\"parameter\"] = \"\";\n    ASSERT_FALSE(_accessor->validate_against_schema(mutable_cfg, JsonSection::CV_GATE));\n    cv_in[\"parameter\"] = \"pitch\";\n    cv_in[\"processor\"] = \"\";\n    ASSERT_FALSE(_accessor->validate_against_schema(mutable_cfg, JsonSection::CV_GATE));\n    cv_in[\"processor\"] = \"synth\";\n\n    rapidjson::Value& gate_out = mutable_cfg[\"cv_control\"][\"gate_outputs\"][0];\n    gate_out[\"mode\"] = \"sync__\";\n    ASSERT_FALSE(_accessor->validate_against_schema(mutable_cfg, JsonSection::CV_GATE));\n    gate_out[\"mode\"] = \"note_event\";\n    gate_out[\"channel\"] = 1234;\n    ASSERT_FALSE(_accessor->validate_against_schema(mutable_cfg, JsonSection::CV_GATE));\n}\n\nTEST_F(TestJsonConfigurator, TestInititalStateSchema)\n{\n    auto [status, test_cfg] = _accessor->parse_section(JsonSection::STATE);\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n\n    rapidjson::Document mutable_cfg;\n    mutable_cfg.SetObject();\n    rapidjson::Value val(rapidjson::kObjectType);\n\n    mutable_cfg.AddMember(\"initial_state\", val, mutable_cfg.GetAllocator());\n    mutable_cfg[\"initial_state\"].CopyFrom(test_cfg, mutable_cfg.GetAllocator());\n\n    auto& state_1 = mutable_cfg[\"initial_state\"].GetArray()[0];\n    ASSERT_TRUE(_accessor->validate_against_schema(mutable_cfg, JsonSection::STATE));\n    state_1[\"parameters\"][\"pan\"] = 1.5;\n    ASSERT_FALSE(_accessor->validate_against_schema(mutable_cfg, JsonSection::STATE));\n    state_1[\"parameters\"][\"pan\"] = \"0.37\";\n    ASSERT_FALSE(_accessor->validate_against_schema(mutable_cfg, JsonSection::STATE));\n    state_1[\"parameters\"][\"pan\"] = 0.37;\n    state_1[\"program\"] = \"string\";\n    ASSERT_FALSE(_accessor->validate_against_schema(mutable_cfg, JsonSection::STATE));\n    state_1[\"program\"] = 5;\n    state_1[\"bypassed\"] = \"off\";\n    ASSERT_FALSE(_accessor->validate_against_schema(mutable_cfg, JsonSection::STATE));\n    state_1[\"bypassed\"] = true;\n    ASSERT_TRUE(_accessor->validate_against_schema(mutable_cfg, JsonSection::STATE));\n }\n\nTEST_F(TestJsonConfigurator, TestLoadEventList)\n{\n    // Load the tracks first so we can find the processors\n    ASSERT_EQ(JsonConfigReturnStatus::OK, _module_under_test->load_tracks());\n\n    auto [status, events] = _module_under_test->load_event_list();\n    ASSERT_EQ(JsonConfigReturnStatus::OK, status);\n    ASSERT_EQ(4u, events.size());\n}\n\nTEST(TestSchemaValidation, TestSchemaMetaValidation)\n{\n    // RapidJson only validates the schemas to be valid json, not that they\n    // actually follow a valid schema.\n\n    const char* meta_schema_char =\n        #include \"test_utils/meta_schema_v4.json\"\n        ;\n\n    rapidjson::Document meta_schema;\n    meta_schema.Parse(meta_schema_char);\n    ASSERT_FALSE(meta_schema.HasParseError());\n    rapidjson::SchemaDocument schema_document(meta_schema);\n    rapidjson::SchemaValidator validator(schema_document);\n\n    rapidjson::Document schema;\n    schema.Parse(section_schema(JsonSection::HOST_CONFIG));\n    ASSERT_FALSE(schema.HasParseError());\n    ASSERT_TRUE(schema.Accept(validator));\n\n    schema.Parse(section_schema(JsonSection::TRACKS));\n    ASSERT_FALSE(schema.HasParseError());\n    ASSERT_TRUE(schema.Accept(validator));\n\n    schema.Parse(section_schema(JsonSection::MIDI));\n    ASSERT_FALSE(schema.HasParseError());\n    ASSERT_TRUE(schema.Accept(validator));\n\n    schema.Parse(section_schema(JsonSection::OSC));\n    ASSERT_FALSE(schema.HasParseError());\n    ASSERT_TRUE(schema.Accept(validator));\n\n    schema.Parse(section_schema(JsonSection::CV_GATE));\n    ASSERT_FALSE(schema.HasParseError());\n    ASSERT_TRUE(schema.Accept(validator));\n\n    schema.Parse(section_schema(JsonSection::EVENTS));\n    ASSERT_FALSE(schema.HasParseError());\n    ASSERT_TRUE(schema.Accept(validator));\n\n    schema.Parse(section_schema(JsonSection::STATE));\n    ASSERT_FALSE(schema.HasParseError());\n    ASSERT_TRUE(schema.Accept(validator));\n\n}"
  },
  {
    "path": "test/unittests/engine/midi_dispatcher_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"test_utils/engine_mockup.h\"\n#include \"test_utils/mock_midi_frontend.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"engine/midi_dispatcher.cpp\"\n\nusing ::testing::NiceMock;\nusing ::testing::_;\n\nusing namespace midi;\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\nusing namespace sushi::internal::midi_dispatcher;\n\nconstexpr float TEST_SAMPLE_RATE = 48000.0;\n\nconst MidiDataByte TEST_NOTE_ON_CH2   = {0x91, 62, 55, 0}; /* Channel 2 */\nconst MidiDataByte TEST_NOTE_OFF_CH3  = {0x82, 60, 45, 0}; /* Channel 3 */\nconst MidiDataByte TEST_CTRL_CH_CH4_67 = {0xB3, 67, 75, 0}; /* Channel 4, cc 67 */\nconst MidiDataByte TEST_CTRL_CH_CH4_68 = {0xB3, 68, 75, 0}; /* Channel 4, cc 68 */\nconst MidiDataByte TEST_CTRL_CH_CH5_2 = {0xB4, 40, 75, 0}; /* Channel 5, cc 40 */\nconst MidiDataByte TEST_CTRL_CH_CH5_3 = {0xB4, 39, 75, 0}; /* Channel 5, cc 39 */\nconst MidiDataByte TEST_PRG_CH_CH5   =  {0xC4, 40, 0, 0};  /* Channel 5, prg 40 */\nconst MidiDataByte TEST_PRG_CH_CH4_2  = {0xC3, 45, 0, 0};  /* Channel 4, prg 45 */\n\nTEST(TestMidiDispatcherEventCreation, TestMakeNoteOnEvent)\n{\n    InputConnection connection = {25, 26, 0, 1, false, 64};\n    NoteOnMessage message = {1, 46, 64};\n    auto event = make_note_on_event(connection, message, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(event->is_keyboard_event());\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    auto typed_event = static_cast<KeyboardEvent*>(event.get());\n    EXPECT_EQ(KeyboardEvent::Subtype::NOTE_ON, typed_event->subtype());\n    EXPECT_EQ(25u, typed_event->processor_id());\n    EXPECT_EQ(1, typed_event->channel());\n    EXPECT_EQ(46, typed_event->note());\n    EXPECT_NEAR(0.5, typed_event->velocity(), 0.05);\n}\n\nTEST(TestMidiDispatcherEventCreation, TestMakeNoteOnWithZeroVelEvent)\n{\n    InputConnection connection = {25, 26, 0, 1, false, 64};\n    NoteOnMessage message = {1, 60, 0};\n    auto event = make_note_on_event(connection, message, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(event->is_keyboard_event());\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    auto typed_event = static_cast<KeyboardEvent*>(event.get());\n    EXPECT_EQ(KeyboardEvent::Subtype::NOTE_OFF, typed_event->subtype());\n    EXPECT_EQ(25u, typed_event->processor_id());\n    EXPECT_EQ(1, typed_event->channel());\n    EXPECT_EQ(60, typed_event->note());\n    EXPECT_NEAR(0.5, typed_event->velocity(), 0.05);\n}\n\nTEST(TestMidiDispatcherEventCreation, TestMakeNoteOffEvent)\n{\n    InputConnection connection = {25, 26, 0, 1, false, 64};\n    NoteOffMessage message = {2, 46, 64};\n    auto event = make_note_off_event(connection, message, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(event->is_keyboard_event());\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    auto typed_event = static_cast<KeyboardEvent*>(event.get());\n    EXPECT_EQ(KeyboardEvent::Subtype::NOTE_OFF, typed_event->subtype());\n    EXPECT_EQ(25u, typed_event->processor_id());\n    EXPECT_EQ(2, typed_event->channel());\n    EXPECT_EQ(46, typed_event->note());\n    EXPECT_NEAR(0.5, typed_event->velocity(), 0.05);\n}\n\nTEST(TestMidiDispatcherEventCreation, TestMakeWrappedMidiEvent)\n{\n    InputConnection connection = {25, 26, 0, 1, false, 64};\n    uint8_t message[] = {3, 46, 64};\n    auto event = make_wrapped_midi_event(connection, message, sizeof(message), IMMEDIATE_PROCESS);\n    EXPECT_TRUE(event->is_keyboard_event());\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    auto typed_event = static_cast<KeyboardEvent*>(event.get());\n    EXPECT_EQ(KeyboardEvent::Subtype::WRAPPED_MIDI, typed_event->subtype());\n    EXPECT_EQ(25u, typed_event->processor_id());\n    EXPECT_EQ(3u, typed_event->midi_data()[0]);\n    EXPECT_EQ(46u, typed_event->midi_data()[1]);\n    EXPECT_EQ(64u, typed_event->midi_data()[2]);\n    EXPECT_EQ(0u, typed_event->midi_data()[3]);\n}\n\nTEST(TestMidiDispatcherEventCreation, TestMakeParameterChangeEvent)\n{\n    InputConnection connection = {25, 26, 0, 1, false, 64};\n    ControlChangeMessage message = {1, 50, 32};\n    auto event = make_param_change_event(connection, message, IMMEDIATE_PROCESS);\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    auto typed_event = static_cast<ParameterChangeEvent*>(event.get());\n    EXPECT_EQ(25u, typed_event->processor_id());\n    EXPECT_EQ(26u, typed_event->parameter_id());\n    EXPECT_NEAR(0.25, typed_event->float_value(), 0.01);\n}\n\nTEST(TestMidiDispatcherEventCreation, TestMakeProgramChangeEvent)\n{\n    InputConnection connection = {25, 0, 0, 0, false, 64};\n    ProgramChangeMessage message = {1, 32};\n    auto event = make_program_change_event(connection, message, IMMEDIATE_PROCESS);\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    auto typed_event = static_cast<ProgramChangeEvent*>(event.get());\n    EXPECT_EQ(25u, typed_event->processor_id());\n    EXPECT_EQ(32, typed_event->program_no());\n}\n\nclass TestMidiDispatcher : public ::testing::Test\n{\nprotected:\n    TestMidiDispatcher() = default;\n\n    void SetUp() override\n    {\n        _module_under_test.set_frontend(&_mock_frontend);\n    }\n\n    EventDispatcherMockup _test_dispatcher;\n    EngineMockup          _test_engine {TEST_SAMPLE_RATE};\n    ::testing::NiceMock<MockMidiFrontend> _mock_frontend{nullptr};\n    MidiDispatcher _module_under_test{&_test_dispatcher};\n};\n\nTEST_F(TestMidiDispatcher, TestKeyboardDataConnection)\n{\n    auto track_1 = _test_engine.processor_container()->track(\"track 1\");\n    ObjectId track_id_1 = track_1->id();\n    auto track_2 = _test_engine.processor_container()->track(\"track 2\");\n    ObjectId track_id_2 = track_2->id();\n\n    auto input_connections = _module_under_test.get_all_kb_input_connections();\n    EXPECT_TRUE(input_connections.size()==0);\n\n    /* Send midi message without connections */\n    _module_under_test.send_midi(1, TEST_NOTE_ON_CH2, IMMEDIATE_PROCESS);\n    _module_under_test.send_midi(0, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Connect all midi channels (OMNI) */\n    _module_under_test.set_midi_inputs(5);\n    _module_under_test.connect_kb_to_track(1, track_id_1);\n    _module_under_test.send_midi(1, TEST_NOTE_ON_CH2, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher.got_event());\n\n    _module_under_test.send_midi(0, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Disconnect OMNI */\n    _module_under_test.disconnect_kb_from_track(1, track_id_1);\n\n    _module_under_test.send_midi(1, TEST_NOTE_ON_CH2, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Connect with a specific midi channel (3) */\n    _module_under_test.connect_kb_to_track(2,\n                                           track_id_2,\n                                           midi::MidiChannel::CH_3);\n    _module_under_test.send_midi(2, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher.got_event());\n\n    _module_under_test.send_midi(2, TEST_NOTE_ON_CH2, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Test fetching connections */\n    input_connections = _module_under_test.get_all_kb_input_connections();\n    EXPECT_TRUE(input_connections.size() == 1);\n\n    /* Disconnect specific midi channel */\n    _module_under_test.disconnect_kb_from_track(2,\n                                                track_id_2,\n                                                midi::MidiChannel::CH_3);\n\n    _module_under_test.send_midi(2, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    input_connections = _module_under_test.get_all_kb_input_connections();\n    EXPECT_TRUE(input_connections.size() == 0);\n}\n\nTEST_F(TestMidiDispatcher, TestKeyboardDataOutConnection)\n{\n    auto track = _test_engine.processor_container()->track(\"track 1\");\n    ObjectId track_id = track->id();\n\n    auto output_connections = _module_under_test.get_all_kb_output_connections();\n    EXPECT_TRUE(output_connections.size()==0);\n\n    KeyboardEvent event_ch12(KeyboardEvent::Subtype::NOTE_ON,\n                             track_id,\n                             12,\n                             48,\n                             0.5f,\n                             IMMEDIATE_PROCESS);\n\n    KeyboardEvent event_ch5(KeyboardEvent::Subtype::NOTE_ON,\n                            track_id,\n                            5,\n                            48,\n                            0.5f,\n                            IMMEDIATE_PROCESS);\n\n    /* Send midi message without connections */\n    auto event = std::make_unique<KeyboardEvent>(event_ch12);\n    auto status = _module_under_test.process(event.get());\n    EXPECT_EQ(EventStatus::HANDLED_OK, status);\n\n    /* Connect track to output 1, channel 5 */\n    _module_under_test.set_midi_outputs(3);\n    auto ret = _module_under_test.connect_track_to_output(1,\n                                                          track_id,\n                                                          midi::MidiChannel::CH_5);\n    ASSERT_EQ(MidiDispatcherStatus::OK, ret);\n\n    /* Expect a midi output message */\n    EXPECT_CALL(_mock_frontend, send_midi(1, midi::encode_note_on(4, 48, 0.5f), _)).Times(1);\n    event = std::make_unique<KeyboardEvent>(event_ch5);\n    status = _module_under_test.process(event.get());\n    EXPECT_EQ(EventStatus::HANDLED_OK, status);\n\n    output_connections = _module_under_test.get_all_kb_output_connections();\n    EXPECT_TRUE(output_connections.size() == 1);\n\n    ret = _module_under_test.disconnect_track_from_output(1,\n                                                          track_id,\n                                                          midi::MidiChannel::CH_5);\n    ASSERT_EQ(MidiDispatcherStatus::OK, ret);\n\n    event = std::make_unique<KeyboardEvent>(event_ch5);\n    status = _module_under_test.process(event.get());\n    EXPECT_EQ(EventStatus::HANDLED_OK, status);\n\n    event = std::make_unique<KeyboardEvent>(event_ch12);\n    status = _module_under_test.process(event.get());\n    EXPECT_EQ(EventStatus::HANDLED_OK, status);\n\n    output_connections = _module_under_test.get_all_kb_output_connections();\n    EXPECT_TRUE(output_connections.size() == 0);\n}\n\nTEST_F(TestMidiDispatcher, TestTransportOutputs)\n{\n    _module_under_test.set_midi_outputs(2);\n    EXPECT_FALSE(_module_under_test.midi_clock_enabled(0));\n    EXPECT_FALSE(_module_under_test.midi_clock_enabled(1));\n    auto status = _module_under_test.enable_midi_clock(true, 1);\n    EXPECT_EQ(MidiDispatcherStatus::OK, status);\n    status = _module_under_test.enable_midi_clock(true, 123);\n    EXPECT_NE(MidiDispatcherStatus::OK, status);\n    EXPECT_FALSE(_module_under_test.midi_clock_enabled(0));\n    EXPECT_TRUE(_module_under_test.midi_clock_enabled(1));\n\n    auto start_event = std::make_unique<PlayingModeNotificationEvent>(PlayingMode::PLAYING, IMMEDIATE_PROCESS);\n    auto stop_event = std::make_unique<PlayingModeNotificationEvent>(PlayingMode::STOPPED, IMMEDIATE_PROCESS);\n    auto rec_event = std::make_unique<PlayingModeNotificationEvent>(PlayingMode::RECORDING, IMMEDIATE_PROCESS);\n    auto tick_event = std::make_unique<EngineTimingTickNotificationEvent>(0, IMMEDIATE_PROCESS);\n\n    EXPECT_CALL(_mock_frontend, send_midi(1, midi::encode_start_message(), _)).Times(1);\n    EXPECT_CALL(_mock_frontend, send_midi(1, midi::encode_stop_message(), _)).Times(1);\n    EXPECT_CALL(_mock_frontend, send_midi(1, midi::encode_timing_clock(), _)).Times(1);\n\n    _module_under_test.process(start_event.get());\n    _module_under_test.process(stop_event.get());\n    _module_under_test.process(rec_event.get());\n    _module_under_test.process(tick_event.get());\n}\n\nTEST_F(TestMidiDispatcher, TestRawDataConnection)\n{\n    auto track_1 = _test_engine.processor_container()->track(\"track 1\");\n    ObjectId track_id_1 = track_1->id();\n    auto track_2 = _test_engine.processor_container()->track(\"track 2\");\n    ObjectId track_id_2 = track_2->id();\n\n    /* Send midi message without connections */\n    _module_under_test.send_midi(1, TEST_NOTE_ON_CH2, IMMEDIATE_PROCESS);\n    _module_under_test.send_midi(0, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Connect all midi channels (OMNI) */\n    _module_under_test.set_midi_inputs(5);\n    _module_under_test.connect_raw_midi_to_track(1, track_id_1);\n    _module_under_test.send_midi(1, TEST_NOTE_ON_CH2, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher.got_event());\n\n    _module_under_test.send_midi(0, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Disconnect OMNI */\n    _module_under_test.disconnect_raw_midi_from_track(1, track_id_1);\n    _module_under_test.send_midi(1, TEST_NOTE_ON_CH2, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Connect with a specific midi channel (3) */\n    _module_under_test.connect_raw_midi_to_track(2,\n                                                 track_id_2,\n                                                 midi::MidiChannel::CH_3);\n\n    _module_under_test.send_midi(2, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher.got_event());\n\n    _module_under_test.send_midi(2, TEST_NOTE_ON_CH2, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Disconnect specific midi channel */\n    _module_under_test.disconnect_raw_midi_from_track(2,\n                                                      track_id_1,\n                                                      midi::MidiChannel::CH_3);\n\n    _module_under_test.send_midi(2, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n}\n\nTEST_F(TestMidiDispatcher, TestCCDataConnection)\n{\n    // The id for the mock processor is generated by a static atomic counter in BaseIdGenetator, so needs to be fetched.\n    auto processor = _test_engine.processor_container()->processor(\"processor\");\n    ObjectId processor_id = processor->id();\n\n    const auto parameter = processor->parameter_from_name(\"param 1\");\n    ObjectId parameter_id = parameter->id();\n\n    /* Test with no connections set */\n    _module_under_test.send_midi(1, TEST_CTRL_CH_CH4_67, IMMEDIATE_PROCESS);\n    _module_under_test.send_midi(5, TEST_CTRL_CH_CH4_67, IMMEDIATE_PROCESS);\n    _module_under_test.send_midi(1, TEST_CTRL_CH_CH5_2, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Connect all midi channels (OMNI) */\n    _module_under_test.set_midi_inputs(5);\n    _module_under_test.connect_cc_to_parameter(1,\n                                               processor_id,\n                                               parameter_id,\n                                               67,\n                                               0,\n                                               100,\n                                               false);\n\n    _module_under_test.send_midi(1, TEST_CTRL_CH_CH4_67, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher.got_event());\n\n    /* Send on a different input and a msg with a different cc no */\n    _module_under_test.send_midi(5, TEST_CTRL_CH_CH4_67, IMMEDIATE_PROCESS);\n    _module_under_test.send_midi(1, TEST_CTRL_CH_CH5_2, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Disconnect OMNI */\n    _module_under_test.disconnect_cc_from_parameter(1,\n                                                    processor_id,\n                                                    67);\n\n    _module_under_test.send_midi(1, TEST_CTRL_CH_CH4_67, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Connect with a specific midi channel (5) */\n    _module_under_test.connect_cc_to_parameter(1,\n                                               processor_id,\n                                               parameter_id,\n                                               40,\n                                               0,\n                                               100,\n                                               false,\n                                               midi::MidiChannel::CH_5);\n\n    _module_under_test.send_midi(1, TEST_CTRL_CH_CH5_2, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher.got_event());\n\n    _module_under_test.send_midi(1, TEST_CTRL_CH_CH4_67, IMMEDIATE_PROCESS);\n    _module_under_test.send_midi(2, TEST_CTRL_CH_CH5_2, IMMEDIATE_PROCESS);\n    _module_under_test.send_midi(1, TEST_CTRL_CH_CH5_3, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    _module_under_test.send_midi(1, TEST_CTRL_CH_CH4_68, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    _module_under_test.connect_cc_to_parameter(1,\n                                               processor_id,\n                                               parameter_id,\n                                               68,\n                                               0,\n                                               100,\n                                               false,\n                                               midi::MidiChannel::CH_4);\n\n    _module_under_test.send_midi(1, TEST_CTRL_CH_CH4_68, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher.got_event());\n\n    /* Test fetching connections */\n    auto input_connections = _module_under_test.get_all_cc_input_connections();\n    EXPECT_TRUE(input_connections.size()==2);\n\n    /* Tests fetching using a non-existent processor ID */\n    auto input_connection = _module_under_test.get_cc_input_connections_for_processor(1);\n    EXPECT_TRUE(input_connection.size()==0);\n\n    /* Disconnect specific channel */\n    _module_under_test.disconnect_cc_from_parameter(1,\n                                                    processor_id,\n                                                    40,\n                                                    midi::MidiChannel::CH_5);\n\n    _module_under_test.send_midi(1, TEST_CTRL_CH_CH5_2, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    _module_under_test.send_midi(1, TEST_CTRL_CH_CH4_68, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher.got_event());\n}\n\nTEST_F(TestMidiDispatcher, TestProgramChangeConnection)\n{\n    auto processor = _test_engine.processor_container()->processor(\"processor\");\n    ObjectId processor_id = processor->id();\n\n    /* Send midi message without connections */\n    _module_under_test.send_midi(1, TEST_PRG_CH_CH5, IMMEDIATE_PROCESS);\n    _module_under_test.send_midi(0, TEST_PRG_CH_CH5, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Connect all midi channels (OMNI) */\n    _module_under_test.set_midi_inputs(5);\n    _module_under_test.connect_pc_to_processor(1, processor_id);\n    _module_under_test.send_midi(1, TEST_PRG_CH_CH5, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher.got_event());\n\n    _module_under_test.send_midi(0, TEST_NOTE_OFF_CH3, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Disconnect OMNI */\n    _module_under_test.disconnect_pc_from_processor(1, processor_id);\n\n    _module_under_test.send_midi(1, TEST_PRG_CH_CH5, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Connect with a specific midi channel (4) */\n    _module_under_test.connect_pc_to_processor(2,\n                                               processor_id,\n                                               midi::MidiChannel::CH_4);\n\n    _module_under_test.send_midi(2, TEST_PRG_CH_CH4_2, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(_test_dispatcher.got_event());\n\n    _module_under_test.send_midi(2, TEST_PRG_CH_CH5, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n\n    /* Test fetching connections */\n    auto input_connections = _module_under_test.get_all_pc_input_connections();\n    EXPECT_TRUE(input_connections.size()==1);\n\n    /* Tests fetching using a non-existent processor ID */\n    auto input_connection = _module_under_test.get_pc_input_connections_for_processor(2000);\n    EXPECT_TRUE(input_connection.size()==0);\n\n    /* Disconnect specific channel */\n    _module_under_test.disconnect_pc_from_processor(2,\n                                                     processor_id,\n                                                     midi::MidiChannel::CH_4);\n\n    _module_under_test.send_midi(2, TEST_PRG_CH_CH4_2, IMMEDIATE_PROCESS);\n    EXPECT_FALSE(_test_dispatcher.got_event());\n}"
  },
  {
    "path": "test/unittests/engine/parameter_manager_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n#include \"gmock/gmock.h\"\n#include \"gmock/gmock-actions.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"engine/parameter_manager.cpp\"\n\n#include \"plugins/gain_plugin.h\"\n\n#include \"test_utils/mock_event_dispatcher.h\"\n#include \"test_utils/mock_processor_container.h\"\n#include \"test_utils/host_control_mockup.h\"\n\nnamespace sushi::internal\n{\n\nclass ParameterManagerAccessor\n{\npublic:\n    explicit ParameterManagerAccessor(ParameterManager& plugin) : _plugin(plugin) {}\n\n    // Not const: it's altered in the tests.\n    [[nodiscard]] ParameterManager::Parameters& parameters()\n    {\n        return _plugin._parameters;\n    }\n\nprivate:\n    ParameterManager& _plugin;\n};\n\n} // end namespace sushi::internal\n\nusing namespace sushi;\nusing namespace sushi::internal;\n\nusing ::testing::Return;\nusing ::testing::AllOf;\nusing ::testing::Field;\nusing ::testing::Args;\nusing ::testing::_;\nusing ::testing::Eq;\n\nconstexpr auto TEST_TRACK_NAME = \"track\";\nconstexpr auto TEST_PROCESSOR_NAME = \"proc\";\nconstexpr float TEST_SAMPLE_RATE = 44100;\nconstexpr Time TEST_MAX_INTERVAL = std::chrono::milliseconds(10);\n\n// Custom Matcher to check the returned events\nMATCHER_P5(ParameterChangeNotificationMatcherFull, proc_id, param_id, norm_val, dom_val, txt_val, \"\")\n{\n    auto typed_ev = static_cast<ParameterChangeNotificationEvent*>(arg.get());\n    return arg->is_parameter_change_notification() &&\n           typed_ev->processor_id() == proc_id &&\n           typed_ev->parameter_id() == param_id &&\n           typed_ev->normalized_value() == norm_val &&\n           typed_ev->domain_value() == dom_val &&\n           typed_ev->formatted_value() == txt_val;\n}\n\nMATCHER_P3(ParameterChangeNotificationMatcher, proc_id, param_id, norm_val, \"\")\n{\n    auto typed_ev = static_cast<ParameterChangeNotificationEvent*>(arg.get());\n    return arg->is_parameter_change_notification() &&\n           typed_ev->processor_id() == proc_id &&\n           typed_ev->parameter_id() == param_id &&\n           typed_ev->normalized_value() == norm_val;\n}\n\n\nclass TestParameterManager : public ::testing::Test\n{\nprotected:\n    using ::testing::Test::SetUp; // Hide error of hidden overload of virtual function in clang when signatures differ but the name is the same\n\n    TestParameterManager() = default;\n\n    void SetUp() override\n    {\n        _test_processor = std::make_shared<gain_plugin::GainPlugin>(_host_control_mockup.make_host_control_mockup());\n        _test_track = std::make_shared<Track>(_host_control_mockup.make_host_control_mockup(), 2, nullptr);\n        ASSERT_EQ(ProcessorReturnCode::OK, _test_processor->init(TEST_SAMPLE_RATE));\n        ASSERT_EQ(ProcessorReturnCode::OK, _test_track->init(TEST_SAMPLE_RATE));\n        _test_processor->set_name(TEST_PROCESSOR_NAME);\n        _test_track->set_name(TEST_TRACK_NAME);\n\n        // Set up default returns for mock processor container\n        ON_CALL(_mock_processor_container, processor(_test_track->id())).WillByDefault(Return(_test_track));\n        ON_CALL(_mock_processor_container, processor(_test_processor->id())).WillByDefault(Return(_test_processor));\n\n        _module_under_test.track_parameters(_test_processor->id());\n        _module_under_test.track_parameters(_test_track->id());\n    }\n\n    ::testing::NiceMock<MockEventDispatcher> _mock_dispatcher;\n    ::testing::NiceMock<MockProcessorContainer> _mock_processor_container;\n\n    ParameterManager _module_under_test {TEST_MAX_INTERVAL, &_mock_processor_container};\n\n    sushi::internal::ParameterManagerAccessor _accessor {_module_under_test};\n\n    HostControlMockup _host_control_mockup;\n    std::shared_ptr<Processor> _test_processor;\n    std::shared_ptr<Track> _test_track;\n};\n\nTEST_F(TestParameterManager, TestEventCreation)\n{\n    EXPECT_CALL(_mock_dispatcher, dispatch(ParameterChangeNotificationMatcherFull(3u, 4u, 0.5f, 5.0f, \"5.0\"))).Times(1);\n    send_parameter_notification(3u, 4u, 0.5f, 5.0f, \"5.0\", &_mock_dispatcher);\n}\n\nTEST_F(TestParameterManager, TestParameterUpdates)\n{\n    _test_track->process_event(RtEvent::make_parameter_change_event(0, 0, 0, 0.7f));\n    _test_processor->process_event(RtEvent::make_parameter_change_event(0, 0, 0, 0.6f));\n    _module_under_test.mark_parameter_changed(_test_processor->id(), 0, TEST_MAX_INTERVAL);\n    _module_under_test.mark_parameter_changed(_test_track->id(), 0, TEST_MAX_INTERVAL + Time(1));\n\n    // Expect no notifications because time has not yet reached TEST_MAX_INTERVAL\n    EXPECT_CALL(_mock_dispatcher, dispatch(_)).Times(0);\n    _module_under_test.output_parameter_notifications(&_mock_dispatcher, Time(1));\n\n    // Expect 1 notification from test_processor\n    EXPECT_CALL(_mock_dispatcher, dispatch(ParameterChangeNotificationMatcher(_test_processor->id(), 0u, 0.6f))).Times(1);\n    _module_under_test.output_parameter_notifications(&_mock_dispatcher, TEST_MAX_INTERVAL );\n\n    // Expect the other notification from test_track\n    EXPECT_CALL(_mock_dispatcher, dispatch(ParameterChangeNotificationMatcher(_test_track->id(), 0u, 0.7f))).Times(1);\n    _module_under_test.output_parameter_notifications(&_mock_dispatcher, TEST_MAX_INTERVAL + Time(3));\n\n    // Expect no notifications as nothing has changed\n    EXPECT_CALL(_mock_dispatcher, dispatch(_)).Times(0);\n    _module_under_test.output_parameter_notifications(&_mock_dispatcher, TEST_MAX_INTERVAL + Time(5));\n\n    // Change a parameter, still expect no notification as one was sent too recently\n    _test_track->process_event(RtEvent::make_parameter_change_event(_test_track->id(), 0, 0, 0.3f));\n    _module_under_test.mark_parameter_changed(_test_track->id(), 0, 2 * TEST_MAX_INTERVAL);\n    EXPECT_CALL(_mock_dispatcher, dispatch(_)).Times(0);\n    _module_under_test.output_parameter_notifications(&_mock_dispatcher, 2 * TEST_MAX_INTERVAL);\n\n    // Expect 1 notification as we have advanced time sufficiently.\n    EXPECT_CALL(_mock_dispatcher, dispatch(ParameterChangeNotificationMatcher(_test_track->id(), 0u, 0.3f))).Times(1);\n    _module_under_test.output_parameter_notifications(&_mock_dispatcher, 3 * TEST_MAX_INTERVAL);\n}\n\nTEST_F(TestParameterManager, TestProcessorUpdates)\n{\n    // Change every parameter value\n    for (auto p : _test_track->all_parameters())\n    {\n        _test_track->process_event(RtEvent::make_parameter_change_event(_test_track->id(), 0, p->id(), 0.12345f));\n    }\n    _module_under_test.mark_processor_changed(_test_track->id(), TEST_MAX_INTERVAL);\n\n    // Expect no notifications because time has not yet reached TEST_MAX_INTERVAL\n    EXPECT_CALL(_mock_dispatcher, dispatch(_)).Times(0);\n    _module_under_test.output_parameter_notifications(&_mock_dispatcher, Time(0));\n\n    // Expect 1 notification from every parameter of test_track\n    EXPECT_CALL(_mock_dispatcher, dispatch(_)).Times(_test_track->parameter_count());\n    _module_under_test.output_parameter_notifications(&_mock_dispatcher, 2 * TEST_MAX_INTERVAL);\n\n    // Expect no notifications as nothing has changed\n    EXPECT_CALL(_mock_dispatcher, dispatch(_)).Times(0);\n    _module_under_test.output_parameter_notifications(&_mock_dispatcher, TEST_MAX_INTERVAL + Time(5));\n}\n\nTEST_F(TestParameterManager, TestErrorHandling)\n{\n    // Notify processors that doesn't exist, should not crash, nor output anything\n    _module_under_test.mark_processor_changed(12345, TEST_MAX_INTERVAL);\n    _module_under_test.mark_parameter_changed(2345, 6789, TEST_MAX_INTERVAL);\n\n    EXPECT_CALL(_mock_dispatcher, dispatch(_)).Times(0);\n    _module_under_test.output_parameter_notifications(&_mock_dispatcher, 2 * TEST_MAX_INTERVAL);\n\n    // Notify with a non-existing parameter id, should not crash, nor output anything\n    _module_under_test.mark_parameter_changed(_test_track->id(), 1234, TEST_MAX_INTERVAL);\n\n    EXPECT_CALL(_mock_dispatcher, dispatch(_)).Times(0);\n    _module_under_test.output_parameter_notifications(&_mock_dispatcher, 2 * TEST_MAX_INTERVAL);\n\n    // Force a value change for this particular parameter, we still shouldn't output anything\n    const auto& value = _accessor.parameters()[_test_track->id()].find(1234);\n    if (value != _accessor.parameters()[_test_track->id()].end())\n    {\n        value->second.value = 0.5;\n    }\n\n    _module_under_test.mark_parameter_changed(_test_track->id(), 1234, TEST_MAX_INTERVAL);\n    _module_under_test.output_parameter_notifications(&_mock_dispatcher, 2 * TEST_MAX_INTERVAL);\n}\n"
  },
  {
    "path": "test/unittests/engine/plugin_library_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"engine/plugin_library.cpp\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\n\n#ifdef _MSC_VER\nconstexpr auto PLUGIN_PATH = R\"(C:\\home\\foo\\bar\\my_absolute_plugin.so)\";\nconstexpr auto INEXISTENT_PATH = R\"(C:\\home\\foo\\bar)\";\n#else\nconstexpr auto PLUGIN_PATH = \"/home/foo/bar/my_absolute_plugin.so\";\nconstexpr auto INEXISTENT_PATH = \"/home/foo/bar\";\n#endif\n\nclass TestPluginLibrary : public ::testing::Test\n{\nprotected:\n    TestPluginLibrary() = default;\n\n    engine::PluginLibrary _module_under_test;\n};\n\n\nTEST_F(TestPluginLibrary, TestAbsolutePath)\n{\n    // Check that to_absolute_path() is an identity\n    // if the path is already absolute\n\n    std::string plugin_path{PLUGIN_PATH};\n    ASSERT_EQ(plugin_path, _module_under_test.to_absolute_path(plugin_path));\n}\n\nTEST_F(TestPluginLibrary, TestEmptyPath)\n{\n    // Make sure we don't concatenate empty path with the base path\n\n    ASSERT_EQ(\"\", _module_under_test.to_absolute_path(\"\"));\n}\n\nTEST_F(TestPluginLibrary, TestPathConcatenation)\n{\n    _module_under_test.set_base_plugin_path(INEXISTENT_PATH);\n    ASSERT_EQ(PLUGIN_PATH, _module_under_test.to_absolute_path(\"my_absolute_plugin.so\"));\n}\n\n"
  },
  {
    "path": "test/unittests/engine/processor_container_test.cpp",
    "content": "#include <thread>\n\n#include \"gtest/gtest.h\"\n\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/host_control_mockup.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"engine/processor_container.cpp\"\n\nconstexpr float SAMPLE_RATE = 44000;\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\n\nclass TestProcessorContainer : public ::testing::Test\n{\nprotected:\n    TestProcessorContainer() {}\n\n    HostControlMockup  _hc;\n    ProcessorContainer _module_under_test;\n};\n\nTEST_F(TestProcessorContainer, TestAddingAndRemoving)\n{\n    auto proc_1 = std::make_shared<DummyProcessor>(_hc.make_host_control_mockup(SAMPLE_RATE));\n    proc_1->set_name(\"one\");\n    auto proc_2 = std::make_shared<DummyProcessor>(_hc.make_host_control_mockup(SAMPLE_RATE));\n    proc_2->set_name(\"two\");\n    auto id_1 = proc_1->id();\n    auto id_2 = proc_2->id();\n    ASSERT_TRUE(_module_under_test.add_processor(proc_1));\n    ASSERT_TRUE(_module_under_test.add_processor(proc_2));\n\n    // Assert false when adding proc_2 again\n    ASSERT_FALSE(_module_under_test.add_processor(proc_2));\n\n    // Access these processors\n    ASSERT_TRUE(_module_under_test.processor_exists(id_1));\n    ASSERT_TRUE(_module_under_test.processor_exists(\"two\"));\n    ASSERT_EQ(\"one\", _module_under_test.processor(\"one\")->name());\n    ASSERT_EQ(id_2, _module_under_test.processor(id_2)->id());\n    ASSERT_EQ(proc_2, _module_under_test.mutable_processor(id_2));\n    ASSERT_EQ(proc_1, _module_under_test.mutable_processor(id_1));\n    ASSERT_EQ(2u, _module_under_test.all_processors().size());\n\n    // Access non-existing processors\n    ASSERT_FALSE(_module_under_test.processor_exists(ObjectId(123'456'789)));\n    ASSERT_FALSE(_module_under_test.processor_exists(\"three\"));\n    ASSERT_EQ(nullptr, _module_under_test.processor(\"four\"));\n    ASSERT_EQ(nullptr, _module_under_test.processor(ObjectId(234'567'890)));\n\n    // Remove processors\n    ASSERT_TRUE(_module_under_test.remove_processor(id_1));\n    ASSERT_TRUE(_module_under_test.remove_processor(id_2));\n    ASSERT_FALSE(_module_under_test.remove_processor(id_1));\n\n    ASSERT_FALSE(_module_under_test.processor_exists(id_1));\n    ASSERT_FALSE(_module_under_test.processor_exists(\"two\"));\n    ASSERT_EQ(nullptr, _module_under_test.mutable_processor(id_2));\n    ASSERT_EQ(nullptr, _module_under_test.mutable_processor(id_1));\n}\n\nTEST_F(TestProcessorContainer, TestTrackManagement)\n{\n    auto proc_1 = std::make_shared<DummyProcessor>(_hc.make_host_control_mockup(SAMPLE_RATE));\n    proc_1->set_name(\"one\");\n    auto proc_2 = std::make_shared<DummyProcessor>(_hc.make_host_control_mockup(SAMPLE_RATE));\n    proc_2->set_name(\"two\");\n    auto track = std::make_shared<Track>(_hc.make_host_control_mockup(SAMPLE_RATE), 2, nullptr);\n    track->set_name(\"track\");\n\n    ASSERT_TRUE(_module_under_test.add_processor(proc_1));\n    ASSERT_TRUE(_module_under_test.add_processor(proc_2));\n    ASSERT_TRUE(_module_under_test.add_processor(track));\n\n    ASSERT_TRUE(_module_under_test.add_track(track));\n    ASSERT_FALSE(_module_under_test.add_track(track));\n\n    ASSERT_TRUE(_module_under_test.add_to_track(proc_1, track->id(), std::nullopt));\n    ASSERT_TRUE(_module_under_test.add_to_track(proc_2, track->id(), proc_1->id()));\n\n    ASSERT_TRUE(_module_under_test.processor_exists(track->id()));\n    ASSERT_EQ(track, _module_under_test.track(track->id()));\n    ASSERT_EQ(track, _module_under_test.track(\"track\"));\n    ASSERT_EQ(nullptr, _module_under_test.track(\"two\"));\n\n    auto procs = _module_under_test.processors_on_track(track->id());\n    ASSERT_EQ(2u, procs.size());\n    ASSERT_EQ(\"two\", procs[0]->name());\n    ASSERT_EQ(\"one\", procs[1]->name());\n\n    ASSERT_TRUE(_module_under_test.remove_from_track(proc_2->id(), track->id()));\n    procs = _module_under_test.processors_on_track(track->id());\n    ASSERT_EQ(1u, procs.size());\n    ASSERT_EQ(\"one\", procs[0]->name());\n\n    ASSERT_TRUE(_module_under_test.remove_from_track(proc_1->id(), track->id()));\n    ASSERT_TRUE(_module_under_test.remove_processor(proc_1->id()));\n    ASSERT_TRUE(_module_under_test.remove_processor(proc_2->id()));\n    ASSERT_TRUE(_module_under_test.remove_track(track->id()));\n    ASSERT_TRUE(_module_under_test.remove_processor(track->id()));\n\n    ASSERT_TRUE(_module_under_test.all_tracks().empty());\n    ASSERT_FALSE(_module_under_test.processor_exists(\"track\"));\n    ASSERT_FALSE(_module_under_test.processor_exists(\"one\"));\n    ASSERT_FALSE(_module_under_test.processor_exists(\"two\"));\n}\n"
  },
  {
    "path": "test/unittests/engine/receiver_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"engine/receiver.cpp\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::receiver;\n\nconstexpr auto ZERO_TIMEOUT = std::chrono::milliseconds(0);\n\nclass TestAsyncReceiver : public ::testing::Test\n{\nprotected:\n    TestAsyncReceiver() = default;\n\n    RtSafeRtEventFifo _queue;\n    AsynchronousEventReceiver _module_under_test{&_queue};\n};\n\n\nTEST_F(TestAsyncReceiver, TestBasicHandling)\n{\n    ASSERT_FALSE(_module_under_test.wait_for_response(123u, ZERO_TIMEOUT));\n    auto event = RtEvent::make_insert_processor_event(nullptr);\n    EventId id = event.returnable_event()->event_id();\n    event.returnable_event()->set_handled(true);\n    _queue.push(event);\n    ASSERT_TRUE(_module_under_test.wait_for_response(id, ZERO_TIMEOUT));\n}\n\nTEST_F(TestAsyncReceiver, TestMultipleEvents)\n{\n    ASSERT_FALSE(_module_under_test.wait_for_response(123u, ZERO_TIMEOUT));\n    auto event1 = RtEvent::make_insert_processor_event(nullptr);\n    auto event2 = RtEvent::make_add_processor_to_track_event(123, 234);\n    event1.returnable_event()->set_handled(true);\n    event2.returnable_event()->set_handled(true);\n    EventId id1 = event1.returnable_event()->event_id();\n    EventId id2 = event2.returnable_event()->event_id();\n    _queue.push(event1);\n    _queue.push(event2);\n    // Get the acks in the reverse order to exercise more of the code\n    ASSERT_TRUE(_module_under_test.wait_for_response(id2, ZERO_TIMEOUT));\n    ASSERT_TRUE(_module_under_test.wait_for_response(id1, ZERO_TIMEOUT));\n}"
  },
  {
    "path": "test/unittests/engine/track_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"engine/track.cpp\"\n\n#include \"engine/transport.h\"\n#include \"plugins/passthrough_plugin.h\"\n\n#include \"test_utils/track_accessor.h\"\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/host_control_mockup.h\"\n#include \"test_utils/dummy_processor.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\n\nconstexpr float TEST_SAMPLE_RATE = 48000;\nconstexpr int TEST_CHANNEL_COUNT = 2;\nconstexpr bool CREATE_PAN_CONTROLS = true;\nclass TrackTest : public ::testing::Test\n{\nprotected:\n    TrackTest() = default;\n\n    void SetUp() override\n    {\n        _module_under_test.init(TEST_SAMPLE_RATE);\n    }\n\n    HostControlMockup _host_control;\n    performance::PerformanceTimer _timer;\n\n    Track _module_under_test {_host_control.make_host_control_mockup(),\n                              TEST_CHANNEL_COUNT,\n                              &_timer,\n                              CREATE_PAN_CONTROLS};\n\n    sushi::internal::engine::TrackAccessor _accessor {_module_under_test};\n};\n\nTEST_F(TrackTest, TestMultibusSetup)\n{\n    Track module_under_test((_host_control.make_host_control_mockup()), 2, &_timer);\n    EXPECT_EQ(2, module_under_test.buses());\n    EXPECT_EQ(5, module_under_test.parameter_count());\n    EXPECT_EQ(2, module_under_test.input_bus(1).channel_count());\n    EXPECT_EQ(2, module_under_test.output_bus(1).channel_count());\n}\n\nTEST_F(TrackTest, TestAddAndRemove)\n{\n    DummyProcessor test_processor(_host_control.make_host_control_mockup());\n    DummyProcessor test_processor_2(_host_control.make_host_control_mockup());\n\n    // Add to back\n    auto ok = _module_under_test.add(&test_processor);\n    EXPECT_TRUE(ok);\n    EXPECT_EQ(1u, _accessor.processors().size());\n    EXPECT_FALSE(_module_under_test.remove(1234567u));\n    EXPECT_EQ(1u, _accessor.processors().size());\n\n    // Add test_processor_2 to the front\n    ok = _module_under_test.add(&test_processor_2, test_processor.id());\n    EXPECT_TRUE(ok);\n    EXPECT_EQ(2u, _accessor.processors().size());\n    EXPECT_EQ(&test_processor_2, _accessor.processors()[0]);\n    EXPECT_EQ(&test_processor, _accessor.processors()[1]);\n\n    EXPECT_TRUE(_module_under_test.remove(test_processor.id()));\n    EXPECT_TRUE(_module_under_test.remove(test_processor_2.id()));\n    EXPECT_TRUE(_accessor.processors().empty());\n}\n\nTEST_F(TrackTest, TestNestedBypass)\n{\n    DummyProcessor test_processor(_host_control.make_host_control_mockup());\n    _module_under_test.add(&test_processor);\n    _module_under_test.set_bypassed(true);\n    EXPECT_TRUE(test_processor.bypassed());\n}\n\nTEST_F(TrackTest, TestEmptyChainRendering)\n{\n    auto in_bus = _module_under_test.input_bus(0);\n    test_utils::fill_sample_buffer(in_bus, 1.0f);\n    _module_under_test.render();\n    auto out = _module_under_test.output_bus(0);\n    test_utils::assert_buffer_value(1.0f, out, test_utils::DECIBEL_ERROR);\n}\n\nTEST_F(TrackTest, TestRenderingWithProcessors)\n{\n    passthrough_plugin::PassthroughPlugin plugin(_host_control.make_host_control_mockup());\n    plugin.init(44100);\n    plugin.set_enabled(true);\n    plugin.set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n\n    _module_under_test.add(&plugin);\n\n    auto in_bus = _module_under_test.input_bus(0);\n    test_utils::fill_sample_buffer(in_bus, 1.0f);\n    _module_under_test.render();\n    auto out = _module_under_test.output_bus(0);\n    test_utils::assert_buffer_value(1.0f, out, test_utils::DECIBEL_ERROR);\n}\n\nTEST_F(TrackTest, TestPanAndGain)\n{\n    passthrough_plugin::PassthroughPlugin plugin(_host_control.make_host_control_mockup());\n    plugin.init(44100);\n    plugin.set_enabled(true);\n    plugin.set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n\n    _module_under_test.add(&plugin);\n    auto gain_param = _module_under_test.parameter_from_name(\"gain\");\n    auto pan_param = _module_under_test.parameter_from_name(\"pan\");\n    ASSERT_FALSE(gain_param == nullptr);\n    ASSERT_FALSE(pan_param == nullptr);\n\n    /* Pan hard right and volume up 6 dB */\n    auto gain_ev = RtEvent::make_parameter_change_event(0, 0, gain_param->id(), 0.875);\n    auto pan_ev = RtEvent::make_parameter_change_event(0, 0, pan_param->id(), 1.0f);\n\n    auto in_bus = _module_under_test.input_bus(0);\n    test_utils::fill_sample_buffer(in_bus, 1.0f);\n    _module_under_test.process_event(gain_ev);\n    _module_under_test.process_event(pan_ev);\n\n    _module_under_test.render();\n    auto out = _module_under_test.output_bus(0);\n\n    /* As volume changes will be smoothed, we won't get the exact result. Just verify\n     * that it had an effect. Exact values will be tested by the pan function */\n    EXPECT_LT(out.channel(LEFT_CHANNEL_INDEX)[AUDIO_CHUNK_SIZE-1], 1.0f);\n    EXPECT_GT(out.channel(RIGHT_CHANNEL_INDEX)[AUDIO_CHUNK_SIZE-1], 1.0f);\n}\n\nTEST_F(TrackTest, TestPanAndGainPerBus)\n{\n    Track multibus_track((_host_control.make_host_control_mockup()), 2, &_timer);\n    multibus_track.init(TEST_SAMPLE_RATE);\n\n    auto gain_bus_0 = multibus_track.parameter_from_name(\"gain\");\n    auto gain_bus_1 = multibus_track.parameter_from_name(\"gain_sub_1\");\n    auto pan_bus_0 = multibus_track.parameter_from_name(\"pan\");\n    auto pan_bus_1 = multibus_track.parameter_from_name(\"pan_sub_1\");\n    ASSERT_TRUE(gain_bus_0);\n    ASSERT_TRUE(gain_bus_1);\n    ASSERT_TRUE(pan_bus_0);\n    ASSERT_TRUE(pan_bus_1);\n\n    passthrough_plugin::PassthroughPlugin plugin(_host_control.make_host_control_mockup());\n    plugin.init(44100);\n    plugin.set_enabled(true);\n    plugin.set_channels(multibus_track.input_channels(), multibus_track.input_channels());\n\n    multibus_track.add(&plugin);\n\n    /* Pan hard right/left and volume up/down 6 dB */\n    auto gain_ev_0 = RtEvent::make_parameter_change_event(0, 0, gain_bus_0->id(), 0.875);\n    auto gain_ev_1 = RtEvent::make_parameter_change_event(0, 0, gain_bus_1->id(), 0.875);\n    auto pan_ev_0 = RtEvent::make_parameter_change_event(0, 0, pan_bus_0->id(), 1.0f);\n    auto pan_ev_1 = RtEvent::make_parameter_change_event(0, 0, pan_bus_1->id(), 0.0f);\n\n    auto in_bus = multibus_track.input_bus(0);\n    test_utils::fill_sample_buffer(in_bus, 1.0f);\n    in_bus = multibus_track.input_bus(1);\n    test_utils::fill_sample_buffer(in_bus, 1.0f);\n    multibus_track.process_event(gain_ev_0);\n    multibus_track.process_event(gain_ev_1);\n    multibus_track.process_event(pan_ev_0);\n    multibus_track.process_event(pan_ev_1);\n\n    multibus_track.render();\n    auto out = multibus_track.output_bus(0);\n\n    /* As volume changes will be smoothed, we won't get the exact result. Just verify\n     * that it had an effect. Exact values will be tested by the pan function */\n    EXPECT_LT(out.channel(LEFT_CHANNEL_INDEX)[AUDIO_CHUNK_SIZE-1], 1.0f);\n    EXPECT_GT(out.channel(RIGHT_CHANNEL_INDEX)[AUDIO_CHUNK_SIZE-1], 1.0f);\n    EXPECT_GT(out.channel(2)[AUDIO_CHUNK_SIZE-1], 1.0f);\n    EXPECT_LT(out.channel(3)[AUDIO_CHUNK_SIZE-1], 1.0f);\n}\n\nTEST_F(TrackTest, TestGainOnly)\n{\n    Track gain_only_track((_host_control.make_host_control_mockup()), 4, &_timer, false);\n    gain_only_track.init(TEST_SAMPLE_RATE);\n\n    auto gain_bus_0 = gain_only_track.parameter_from_name(\"gain\");\n    EXPECT_FALSE(gain_only_track.parameter_from_name(\"pan\"));\n    ASSERT_TRUE(gain_bus_0);\n\n    passthrough_plugin::PassthroughPlugin plugin(_host_control.make_host_control_mockup());\n    plugin.init(44100);\n    plugin.set_enabled(true);\n    plugin.set_channels(gain_only_track.input_channels(), gain_only_track.input_channels());\n\n    gain_only_track.add(&plugin);\n\n    /* Volume down 6 dB */\n    auto gain_ev_0 = RtEvent::make_parameter_change_event(0, 0, gain_bus_0->id(), 0.7917f);\n    gain_only_track.process_event(gain_ev_0);\n\n    for (int i = 0; i < gain_only_track.max_input_channels(); ++i)\n    {\n        auto in_bus = gain_only_track.input_channel(i);\n        test_utils::fill_sample_buffer(in_bus, 1.0f);\n    }\n\n    gain_only_track.render();\n    auto out = gain_only_track.output_bus(0);\n\n    /* As volume changes will be smoothed, we won't get the exact result. Just verify\n     * that it had an effect. Exact values will be tested by the pan function */\n    for (int i = 0; i < gain_only_track.max_output_channels(); ++i)\n    {\n        auto in_bus = gain_only_track.output_channel(i);\n        EXPECT_LT(out.channel(0)[AUDIO_CHUNK_SIZE - 1], 1.0f);\n    }\n}\n\nTEST_F(TrackTest, TestMute)\n{\n    passthrough_plugin::PassthroughPlugin plugin(_host_control.make_host_control_mockup());\n    plugin.init(44100);\n    plugin.set_enabled(true);\n    plugin.set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n\n    _module_under_test.add(&plugin);\n    auto mute_param = _module_under_test.parameter_from_name(\"mute\");\n    ASSERT_FALSE(mute_param == nullptr);\n\n    // Mute should be off by default\n    auto in_bus = _module_under_test.input_bus(0);\n    test_utils::fill_sample_buffer(in_bus, 1.0f);\n    _module_under_test.render();\n    test_utils::assert_buffer_value(1.0f, _module_under_test.output_bus(0));\n\n    // Enable mute and run\n    auto mute_event = RtEvent::make_parameter_change_event(0, 0, mute_param->id(), 1.0);\n    _module_under_test.process_event(mute_event);\n    for (int i = 0; i <= TEST_SAMPLE_RATE / AUDIO_CHUNK_SIZE / (500 / GAIN_SMOOTHING_TIME.count()); ++i)\n    {\n        test_utils::fill_sample_buffer(in_bus, 1.0f);\n        _module_under_test.render();\n        EXPECT_LT(_module_under_test.output_bus(0).channel(0)[AUDIO_CHUNK_SIZE - 1], 1.0f);\n    }\n    EXPECT_LT(_module_under_test.output_bus(0).channel(0)[AUDIO_CHUNK_SIZE - 1], 0.1f);\n}\n\nTEST_F(TrackTest, TestEventProcessing)\n{\n    ChunkSampleBuffer buffer(2);\n    RtSafeRtEventFifo event_queue;\n    ASSERT_TRUE(event_queue.empty());\n    passthrough_plugin::PassthroughPlugin plugin(_host_control.make_host_control_mockup());\n    plugin.init(44100);\n    plugin.set_enabled(true);\n    plugin.set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n\n    plugin.set_event_output(&event_queue);\n    _module_under_test.set_channels(2, 2);\n    _module_under_test.set_event_output(&event_queue);\n    _module_under_test.add(&plugin);\n\n    RtEvent event = RtEvent::make_note_on_event(0, 0, 0, 0, 0);\n\n    _module_under_test.process_event(event);\n    _module_under_test.render();\n    ASSERT_FALSE(event_queue.empty());\n    RtEvent e;\n    event_queue.pop(e);\n}\n\nTEST_F(TrackTest, TestEventForwarding)\n{\n    ChunkSampleBuffer buffer(2);\n    RtSafeRtEventFifo event_queue;\n    ASSERT_TRUE(event_queue.empty());\n    passthrough_plugin::PassthroughPlugin plugin(_host_control.make_host_control_mockup());\n    plugin.init(44100);\n    plugin.set_enabled(true);\n    plugin.set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n    plugin.set_event_output(&event_queue);\n\n    _module_under_test.set_event_output(&event_queue);\n    _module_under_test.add(&plugin);\n\n    RtEvent event = RtEvent::make_note_on_event(125, 13, 0, 48, 0.0f);\n\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(buffer, buffer);\n    ASSERT_FALSE(event_queue.empty());\n    RtEvent received_event;\n    bool got_event = event_queue.pop(received_event);\n    ASSERT_TRUE(got_event);\n    auto typed_event = received_event.keyboard_event();\n    ASSERT_EQ(RtEventType::NOTE_ON, received_event.type());\n    /* Assert that the processor id of the event is that of the track and\n     * not the id set. */\n    ASSERT_EQ(_module_under_test.id(), typed_event->processor_id());\n}\n\nTEST_F(TrackTest, TestSilenceUnusedChannels)\n{\n    passthrough_plugin::PassthroughPlugin plugin(_host_control.make_host_control_mockup());\n    plugin.init(44100);\n    plugin.set_enabled(true);\n    plugin.set_channels(1, 1);\n\n    // Add a mono plugin to a stereo track.\n    _module_under_test.add(&plugin);\n\n    // Put some noise in the input buffer\n    auto in_bus = _module_under_test.input_bus(0);\n    test_utils::fill_sample_buffer(in_bus, 1.0f);\n\n    _module_under_test.render();\n    auto out = _module_under_test.output_bus(0);\n\n    auto left_channel = ChunkSampleBuffer::create_non_owning_buffer(out, LEFT_CHANNEL_INDEX, 1);\n    auto right_channel = ChunkSampleBuffer::create_non_owning_buffer(out, RIGHT_CHANNEL_INDEX, 1);\n    test_utils::assert_buffer_value(1.0f, left_channel);\n    test_utils::assert_buffer_value(0.0f, right_channel);\n}\n\nTEST(TestStandAloneFunctions, TesPanAndGainCalculation)\n{\n    auto [left_gain, right_gain] = calc_l_r_gain(5.0f, 0.0f);\n    EXPECT_FLOAT_EQ(5, left_gain);\n    EXPECT_FLOAT_EQ(5, right_gain);\n\n    /* Pan hard right */\n    std::tie(left_gain, right_gain) = calc_l_r_gain(1.0f, 1.0f);\n    EXPECT_FLOAT_EQ(0.0f, left_gain);\n    EXPECT_NEAR(1.41, right_gain, 0.01f);\n\n    /* Pan mid left */\n    std::tie(left_gain, right_gain) = calc_l_r_gain(1.0f, -0.5f);\n    EXPECT_NEAR(1.2f, left_gain, 0.01f);\n    EXPECT_FLOAT_EQ(0.5, right_gain);\n}\n"
  },
  {
    "path": "test/unittests/engine/transport_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"engine/transport.cpp\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"library/rt_event_fifo.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\n\nconstexpr float TEST_SAMPLERATE = 48000;\n\nclass TestTransport : public ::testing::Test\n{\nprotected:\n    TestTransport() = default;\n\n    RtEventFifo<10> _rt_event_output;\n    Transport _module_under_test{TEST_SAMPLERATE, &_rt_event_output};\n};\n\n\nTEST_F(TestTransport, TestBasicQuerying)\n{\n    _module_under_test.set_latency(std::chrono::microseconds(1500));\n    _module_under_test.set_time(std::chrono::seconds(1), 44800);\n\n    EXPECT_EQ(std::chrono::microseconds(1001500), _module_under_test.current_process_time());\n\n    _module_under_test.set_tempo(130, false);\n    _module_under_test.set_time(std::chrono::seconds(0), 0);\n    EXPECT_FLOAT_EQ(130, _module_under_test.current_tempo());\n\n    // Test with too high/negative tempos\n    _module_under_test.set_tempo(130000, false);\n    _module_under_test.set_time(std::chrono::seconds(0), 0);\n    EXPECT_FLOAT_EQ(MAX_TEMPO, _module_under_test.current_tempo());\n\n    _module_under_test.set_tempo(-100, false);\n    _module_under_test.set_time(std::chrono::seconds(0), 0);\n    EXPECT_FLOAT_EQ(MIN_TEMPO, _module_under_test.current_tempo());\n\n    // Test time signatures\n    _module_under_test.set_time_signature({5, 8}, false);\n    EXPECT_EQ(5, _module_under_test.time_signature().numerator);\n    EXPECT_EQ(8, _module_under_test.time_signature().denominator);\n\n    // Test a few invalid time signatures\n    _module_under_test.set_time_signature({-1, 100}, false);\n    EXPECT_EQ(5, _module_under_test.time_signature().numerator);\n    EXPECT_EQ(8, _module_under_test.time_signature().denominator);\n\n    _module_under_test.set_time_signature({1, 0}, false);\n    EXPECT_EQ(5, _module_under_test.time_signature().numerator);\n    EXPECT_EQ(8, _module_under_test.time_signature().denominator);\n\n    _module_under_test.set_position_source(PositionSource::EXTERNAL);\n    EXPECT_EQ(PositionSource::EXTERNAL, _module_under_test.position_source());\n\n    _module_under_test.set_current_beats(1.5);\n    EXPECT_DOUBLE_EQ(1.5, _module_under_test.current_beats());\n}\n\nTEST_F(TestTransport, TestTimeline44Time)\n{\n    constexpr int TEST_SAMPLERATE_X2 = 32768;\n    /* Odd samplerate, but it's a convenient factor of 2 which makes testing easier,\n     * since bar boundaries end up on a power of 2 samplecount if AUDIO_CHUNK_SIZE is\n     * a power of 2*/\n    _module_under_test.set_sample_rate(TEST_SAMPLERATE_X2);\n    _module_under_test.set_time_signature({4, 4}, false);\n    _module_under_test.set_tempo(120, false);\n    _module_under_test.set_playing_mode(PlayingMode::PLAYING, false);\n    _module_under_test.set_time(std::chrono::seconds(0), 0);\n\n    /* Check that the starting point is 0 */\n    EXPECT_DOUBLE_EQ(0.0, _module_under_test.current_bar_beats());\n    EXPECT_DOUBLE_EQ(0.0, _module_under_test.current_beats());\n    EXPECT_DOUBLE_EQ(0.0, _module_under_test.current_bar_start_beats());\n    EXPECT_DOUBLE_EQ(0.0, _module_under_test.current_bar_beats(0));\n    EXPECT_DOUBLE_EQ(0.0, _module_under_test.current_beats(0));\n\n    /* Advance time by 1 second equal to 1/2 bar at 120 bpm */\n    _module_under_test.set_time(std::chrono::seconds(1), TEST_SAMPLERATE_X2 );\n    EXPECT_DOUBLE_EQ(2.0, _module_under_test.current_bar_beats());\n    EXPECT_DOUBLE_EQ(2.0, _module_under_test.current_beats());\n    EXPECT_DOUBLE_EQ(0.0, _module_under_test.current_bar_start_beats());\n\n    /* Test also that offset works correctly */\n    EXPECT_DOUBLE_EQ(3.0, _module_under_test.current_bar_beats(TEST_SAMPLERATE_X2 / 2));\n    EXPECT_DOUBLE_EQ(4.0, _module_under_test.current_beats(TEST_SAMPLERATE_X2));\n\n    /* Advance time by 1.5 second equal to 3/4 bar at 120 bpm  which should bring us\n     * in to the next bar*/\n    _module_under_test.set_time(std::chrono::milliseconds(2500), 5 * TEST_SAMPLERATE_X2 / 2);\n    EXPECT_DOUBLE_EQ(1.0, _module_under_test.current_bar_beats());\n    EXPECT_DOUBLE_EQ(5.0, _module_under_test.current_beats());\n    EXPECT_DOUBLE_EQ(4.0, _module_under_test.current_bar_start_beats());\n}\n\nTEST_F(TestTransport, TestTimeline44TimeWithExternalPositionSource)\n{\n    constexpr int TEST_SAMPLERATE_X2 = 32768;\n    /* Odd samplerate, but it's a convenient factor of 2 which makes testing easier,\n     * since bar boundaries end up on a power of 2 sample count if AUDIO_CHUNK_SIZE is\n     * a power of 2*/\n    _module_under_test.set_sample_rate(TEST_SAMPLERATE_X2);\n    _module_under_test.set_time_signature({4, 4}, false);\n    _module_under_test.set_tempo(120, false);\n    _module_under_test.set_playing_mode(PlayingMode::PLAYING, false);\n    _module_under_test.set_time(std::chrono::seconds(0), 0);\n\n    /* Test skipping internal beat_count_calculation */\n    _module_under_test.set_position_source(PositionSource::EXTERNAL);\n    _module_under_test.set_current_beats(5.1);\n    _module_under_test.set_current_bar_beats(1.1);\n    \n    _module_under_test.set_time(std::chrono::milliseconds(2500), 5 * TEST_SAMPLERATE_X2 / 2);\n\n    EXPECT_DOUBLE_EQ(1.1, _module_under_test.current_bar_beats());\n    EXPECT_DOUBLE_EQ(5.1, _module_under_test.current_beats());\n    EXPECT_DOUBLE_EQ(4.0, _module_under_test.current_bar_start_beats());\n}\n\nTEST_F(TestTransport, TestTimeline68Time)\n{\n    /* Test the above but with different time signature and samplerate */\n    _module_under_test.set_sample_rate(TEST_SAMPLERATE);\n    _module_under_test.set_tempo(180, false);\n    _module_under_test.set_time_signature({6, 8}, false);\n\n    // We cannot assume chunk size is an absolute multiple of samples for all buffer sizes.\n    constexpr float precision = 4.0f * static_cast<float>(AUDIO_CHUNK_SIZE) / TEST_SAMPLERATE;\n\n    /* Check that the starting point is 0 */\n    _module_under_test.set_playing_mode(PlayingMode::PLAYING, false);\n    _module_under_test.set_time(std::chrono::seconds(0), 0);\n    EXPECT_DOUBLE_EQ(0.0f, _module_under_test.current_bar_beats());\n    EXPECT_DOUBLE_EQ(0.0f, _module_under_test.current_beats());\n    EXPECT_DOUBLE_EQ(0.0f, _module_under_test.current_bar_start_beats());\n\n    /* Advance time by 1/2 second equal to 1/2 bar at 180 bpm. Can't test exact\n     * values here since 48000 is not an even multiple of AUDIO_CHUNK_SIZE */\n    _module_under_test.set_time(std::chrono::milliseconds(500), static_cast<int64_t>(TEST_SAMPLERATE / 2.0f));\n    EXPECT_NEAR(1.5, _module_under_test.current_bar_beats(), precision);\n    EXPECT_NEAR(1.5, _module_under_test.current_beats(), precision);\n    EXPECT_NEAR(0.0, _module_under_test.current_bar_start_beats(), precision);\n\n    /* Advance time by 1 second equal to 1 bar at 180 bpm\n     * which should bring us halfway  in to the next bar */\n    _module_under_test.set_time(std::chrono::milliseconds(1500), static_cast<int64_t>(3 * TEST_SAMPLERATE / 2.0f));\n    EXPECT_NEAR(1.5, _module_under_test.current_bar_beats(), precision);\n    EXPECT_NEAR(4.5, _module_under_test.current_beats(), precision);\n    EXPECT_NEAR(3.0, _module_under_test.current_bar_start_beats(), precision);\n}\n\nTEST_F(TestTransport, TestPlayStateChange)\n{\n    _module_under_test.set_sample_rate(TEST_SAMPLERATE);\n    _module_under_test.set_time_signature({4, 4}, false);\n    _module_under_test.set_tempo(120, false);\n    _module_under_test.set_playing_mode(PlayingMode::STOPPED, false);\n    _module_under_test.set_sync_mode(SyncMode::INTERNAL, false);\n    _module_under_test.set_time(std::chrono::seconds(0), 0);\n\n    EXPECT_FALSE(_module_under_test.playing());\n    EXPECT_EQ(PlayStateChange::UNCHANGED, _module_under_test.current_state_change());\n\n    _module_under_test.set_time(std::chrono::seconds(1), 44000);\n    EXPECT_FALSE(_module_under_test.playing());\n    EXPECT_EQ(PlayStateChange::UNCHANGED, _module_under_test.current_state_change());\n\n    _module_under_test.set_playing_mode(PlayingMode::PLAYING, false);\n    _module_under_test.set_time(std::chrono::seconds(2), 88000);\n    EXPECT_TRUE(_module_under_test.playing());\n    EXPECT_EQ(PlayStateChange::STARTING, _module_under_test.current_state_change());\n\n    _module_under_test.set_time(std::chrono::seconds(3), 132000);\n    EXPECT_TRUE(_module_under_test.playing());\n    EXPECT_EQ(PlayStateChange::UNCHANGED, _module_under_test.current_state_change());\n}\n\nTEST_F(TestTransport, TestNotifications)\n{\n    /* Notifications are only sent if the engine is running audio processing, during which,\n     * changes are passed as RtEvents, so they can be applied at the start of an audio chunk */\n    _module_under_test.set_time_signature({4, 4}, false);\n    _module_under_test.set_playing_mode(PlayingMode::STOPPED, false);\n\n    _module_under_test.set_tempo(123, true);\n    _module_under_test.process_event(RtEvent::make_tempo_event(0, 123));\n    _module_under_test.set_time(std::chrono::seconds(0), 0);\n    ASSERT_FALSE(_rt_event_output.empty());\n    auto event =_rt_event_output.pop();\n    EXPECT_EQ(RtEventType::TEMPO, event.type());\n    EXPECT_FLOAT_EQ(123.0f, event.tempo_event()->tempo());\n    EXPECT_FLOAT_EQ(123.0f, _module_under_test.current_tempo());\n\n    _module_under_test.set_time_signature({6, 8}, true);\n    _module_under_test.process_event(RtEvent::make_time_signature_event(0, {6, 8}));\n    _module_under_test.set_time(std::chrono::milliseconds(1), AUDIO_CHUNK_SIZE);\n    ASSERT_FALSE(_rt_event_output.empty());\n    event =_rt_event_output.pop();\n    EXPECT_EQ(RtEventType::TIME_SIGNATURE, event.type());\n    EXPECT_EQ(TimeSignature({6, 8}), event.time_signature_event()->time_signature());\n    EXPECT_EQ(TimeSignature({6, 8}), _module_under_test.time_signature());\n\n    _module_under_test.set_sync_mode(SyncMode::MIDI, true);\n    _module_under_test.process_event(RtEvent::make_sync_mode_event(0, SyncMode::MIDI));\n    _module_under_test.set_time(std::chrono::milliseconds(2), 2 * AUDIO_CHUNK_SIZE);\n    ASSERT_FALSE(_rt_event_output.empty());\n    event =_rt_event_output.pop();\n    EXPECT_EQ(RtEventType::SYNC_MODE, event.type());\n    EXPECT_EQ(SyncMode::MIDI, event.sync_mode_event()->mode());\n    EXPECT_EQ(SyncMode::MIDI, _module_under_test.sync_mode());\n\n    _module_under_test.set_playing_mode(PlayingMode::PLAYING, true);\n    _module_under_test.process_event(RtEvent::make_playing_mode_event(0, PlayingMode::PLAYING));\n    _module_under_test.set_time(std::chrono::milliseconds(3), 3 * AUDIO_CHUNK_SIZE);\n    ASSERT_FALSE(_rt_event_output.empty());\n    event =_rt_event_output.pop();\n    EXPECT_EQ(RtEventType::PLAYING_MODE, event.type());\n    EXPECT_EQ(PlayingMode::PLAYING, event.playing_mode_event()->mode());\n    EXPECT_EQ(PlayingMode::PLAYING, _module_under_test.playing_mode());\n}"
  },
  {
    "path": "test/unittests/library/event_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"library/event.cpp\"\n\n#include \"engine/audio_engine.h\"\n\n#include \"control_frontends/base_control_frontend.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::engine;\nusing namespace sushi::internal::control_frontend;\nusing namespace sushi::internal::midi_dispatcher;\n\nstatic int dummy_processor_callback(void* /*arg*/, EventId /*id*/)\n{\n    return 0;\n}\n\nTEST(EventTest, TestToRtEvent)\n{\n    auto note_on_event = KeyboardEvent(KeyboardEvent::Subtype::NOTE_ON, 1, 0, 48, 1.0f, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(note_on_event.is_keyboard_event());\n    EXPECT_TRUE(note_on_event.maps_to_rt_event());\n    EXPECT_EQ(IMMEDIATE_PROCESS, note_on_event.time());\n    RtEvent rt_event = note_on_event.to_rt_event(5);\n    EXPECT_EQ(RtEventType::NOTE_ON, rt_event.type());\n    EXPECT_EQ(5, rt_event.sample_offset());\n    EXPECT_EQ(1u, rt_event.keyboard_event()->processor_id());\n    EXPECT_EQ(48, rt_event.keyboard_event()->note());\n    EXPECT_EQ(0, rt_event.keyboard_event()->channel());\n    EXPECT_FLOAT_EQ(1.0f, rt_event.keyboard_event()->velocity());\n\n    auto note_off_event = KeyboardEvent(KeyboardEvent::Subtype::NOTE_OFF, 1, 1, 48, 1.0f, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(note_off_event.is_keyboard_event());\n    EXPECT_TRUE(note_off_event.maps_to_rt_event());\n    rt_event = note_off_event.to_rt_event(5);\n    EXPECT_EQ(RtEventType::NOTE_OFF, rt_event.type());\n\n    auto note_at_event = KeyboardEvent(KeyboardEvent::Subtype::NOTE_AFTERTOUCH, 1, 2, 48, 1.0f, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(note_at_event.is_keyboard_event());\n    EXPECT_TRUE(note_at_event.maps_to_rt_event());\n    rt_event = note_at_event.to_rt_event(5);\n    EXPECT_EQ(RtEventType::NOTE_AFTERTOUCH, rt_event.type());\n\n    auto pitchbend_event = KeyboardEvent(KeyboardEvent::Subtype::PITCH_BEND, 2, 3, 0.5f, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(pitchbend_event.is_keyboard_event());\n    rt_event = pitchbend_event.to_rt_event(6);\n    EXPECT_EQ(RtEventType::PITCH_BEND, rt_event.type());\n    EXPECT_EQ(6, rt_event.sample_offset());\n    EXPECT_EQ(2u, rt_event.keyboard_common_event()->processor_id());\n    EXPECT_FLOAT_EQ(0.5f, rt_event.keyboard_common_event()->value());\n\n    auto modulation_event = KeyboardEvent(KeyboardEvent::Subtype::MODULATION, 3, 4, 1.0f, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(modulation_event.is_keyboard_event());\n    EXPECT_TRUE(modulation_event.maps_to_rt_event());\n    rt_event = modulation_event.to_rt_event(5);\n    EXPECT_EQ(RtEventType::MODULATION, rt_event.type());\n\n    auto aftertouch_event = KeyboardEvent(KeyboardEvent::Subtype::AFTERTOUCH, 4, 5, 1.0f, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(aftertouch_event.is_keyboard_event());\n    EXPECT_TRUE(aftertouch_event.maps_to_rt_event());\n    rt_event = aftertouch_event.to_rt_event(5);\n    EXPECT_EQ(RtEventType::AFTERTOUCH, rt_event.type());\n\n    auto midi_event = KeyboardEvent(KeyboardEvent::Subtype::WRAPPED_MIDI, 5, {1,2,3,4}, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(midi_event.is_keyboard_event());\n    rt_event = midi_event.to_rt_event(7);\n    EXPECT_EQ(RtEventType::WRAPPED_MIDI_EVENT, rt_event.type());\n    EXPECT_EQ(7, rt_event.sample_offset());\n    EXPECT_EQ(5u, rt_event.wrapped_midi_event()->processor_id());\n    EXPECT_EQ(MidiDataByte({1,2,3,4}), rt_event.wrapped_midi_event()->midi_data());\n\n    auto param_ch_event = ParameterChangeEvent(ParameterChangeEvent::Subtype::FLOAT_PARAMETER_CHANGE, 6, 50, 1.0f, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(param_ch_event.maps_to_rt_event());\n    rt_event = param_ch_event.to_rt_event(8);\n    EXPECT_EQ(RtEventType::FLOAT_PARAMETER_CHANGE, rt_event.type());\n    EXPECT_EQ(8, rt_event.sample_offset());\n    EXPECT_EQ(6u, rt_event.parameter_change_event()->processor_id());\n    EXPECT_EQ(50u, rt_event.parameter_change_event()->param_id());\n    EXPECT_FLOAT_EQ(1.0f, rt_event.parameter_change_event()->value());\n\n    BlobData testdata = {0, nullptr};\n    auto data_pro_ch_event = DataPropertyEvent(8, 52, testdata, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(data_pro_ch_event.maps_to_rt_event());\n    rt_event = data_pro_ch_event.to_rt_event(10);\n    EXPECT_EQ(RtEventType::DATA_PROPERTY_CHANGE, rt_event.type());\n    EXPECT_EQ(10, rt_event.sample_offset());\n    EXPECT_EQ(8u, rt_event.data_parameter_change_event()->processor_id());\n    EXPECT_EQ(52u, rt_event.data_parameter_change_event()->param_id());\n    EXPECT_EQ(0, rt_event.data_parameter_change_event()->value().size);\n    EXPECT_EQ(nullptr, rt_event.data_parameter_change_event()->value().data);\n\n    auto async_comp_not = AsynchronousProcessorWorkCompletionEvent(123, 9, 53, IMMEDIATE_PROCESS);\n    rt_event = async_comp_not.to_rt_event(11);\n    EXPECT_EQ(RtEventType::ASYNC_WORK_NOTIFICATION, rt_event.type());\n    EXPECT_EQ(123, rt_event.async_work_completion_event()->return_status());\n    EXPECT_EQ(9u, rt_event.async_work_completion_event()->processor_id());\n    EXPECT_EQ(53u, rt_event.async_work_completion_event()->sending_event_id());\n\n    auto bypass_event = SetProcessorBypassEvent(10, true, IMMEDIATE_PROCESS);\n    EXPECT_TRUE(bypass_event.bypass_enabled());\n    rt_event = bypass_event.to_rt_event(12);\n    EXPECT_EQ(RtEventType::SET_BYPASS, rt_event.type());\n    EXPECT_TRUE(rt_event.processor_command_event()->value());\n    EXPECT_EQ(10u, rt_event.processor_command_event()->processor_id());\n}\n\nTEST(EventTest, TestFromRtEvent)\n{\n    auto note_on_event = RtEvent::make_note_on_event(2, 0, 1, 48, 1.0f);\n    auto event = Event::from_rt_event(note_on_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_keyboard_event());\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    auto kb_event = static_cast<KeyboardEvent*>(event.get());\n    EXPECT_EQ(KeyboardEvent::Subtype::NOTE_ON, kb_event->subtype());\n    EXPECT_EQ(1, kb_event->channel());\n    EXPECT_EQ(48, kb_event->note());\n    EXPECT_EQ(2u, kb_event->processor_id());\n    EXPECT_FLOAT_EQ(1.0f, kb_event->value());\n\n    auto note_off_event = RtEvent::make_note_off_event(3, 0, 2, 49, 1.0f);\n    event = Event::from_rt_event(note_off_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_keyboard_event());\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    kb_event = static_cast<KeyboardEvent*>(event.get());\n    EXPECT_EQ(KeyboardEvent::Subtype::NOTE_OFF, kb_event->subtype());\n    EXPECT_EQ(2, kb_event->channel());\n    EXPECT_EQ(49, kb_event->note());\n    EXPECT_EQ(3u, kb_event->processor_id());\n    EXPECT_FLOAT_EQ(1.0f, kb_event->value());\n\n    auto note_at_event = RtEvent::make_note_aftertouch_event(4, 0, 3, 50, 1.0f);\n    event = Event::from_rt_event(note_at_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_keyboard_event());\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    kb_event = static_cast<KeyboardEvent*>(event.get());\n    EXPECT_EQ(KeyboardEvent::Subtype::NOTE_AFTERTOUCH, kb_event->subtype());\n    EXPECT_EQ(3, kb_event->channel());\n    EXPECT_EQ(50, kb_event->note());\n    EXPECT_EQ(4u, kb_event->processor_id());\n    EXPECT_FLOAT_EQ(1.0f, kb_event->value());\n\n    auto mod_event = RtEvent::make_kb_modulation_event(5, 0, 4, 0.5f);\n    event = Event::from_rt_event(mod_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_keyboard_event());\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    kb_event = static_cast<KeyboardEvent*>(event.get());\n    EXPECT_EQ(KeyboardEvent::Subtype::MODULATION, kb_event->subtype());\n    EXPECT_EQ(4, kb_event->channel());\n    EXPECT_EQ(5u, kb_event->processor_id());\n    EXPECT_FLOAT_EQ(0.5f, kb_event->value());\n\n    auto pb_event = RtEvent::make_pitch_bend_event(6, 0, 5, 0.6f);\n    event = Event::from_rt_event(pb_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_keyboard_event());\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    kb_event = static_cast<KeyboardEvent*>(event.get());\n    EXPECT_EQ(KeyboardEvent::Subtype::PITCH_BEND, kb_event->subtype());\n    EXPECT_EQ(5, kb_event->channel());\n    EXPECT_EQ(6u, kb_event->processor_id());\n    EXPECT_FLOAT_EQ(0.6f, kb_event->value());\n\n    auto at_event = RtEvent::make_aftertouch_event(7, 0, 6, 0.7f);\n    event = Event::from_rt_event(at_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_keyboard_event());\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    kb_event = static_cast<KeyboardEvent*>(event.get());\n    EXPECT_EQ(KeyboardEvent::Subtype::AFTERTOUCH, kb_event->subtype());\n    EXPECT_EQ(6, kb_event->channel());\n    EXPECT_EQ(7u, kb_event->processor_id());\n    EXPECT_FLOAT_EQ(0.7f, kb_event->value());\n\n    auto midi_event = RtEvent::make_wrapped_midi_event(8, 0, {1,2,3,4});\n    event = Event::from_rt_event(midi_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_keyboard_event());\n    EXPECT_EQ(IMMEDIATE_PROCESS, event->time());\n    kb_event = static_cast<KeyboardEvent*>(event.get());\n    EXPECT_EQ(KeyboardEvent::Subtype::WRAPPED_MIDI, kb_event->subtype());\n    EXPECT_EQ(8u, kb_event->processor_id());\n    EXPECT_EQ(MidiDataByte({1, 2, 3, 4}), kb_event->midi_data());\n\n    auto tempo_event = RtEvent::make_tempo_event(10, 125);\n    event = Event::from_rt_event(tempo_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_engine_notification());\n    auto tempo_not = static_cast<TempoNotificationEvent*>(event.get());\n    EXPECT_TRUE(tempo_not->is_tempo_notification());\n    EXPECT_FLOAT_EQ(125.0f, tempo_not->tempo());\n\n    auto time_sig_event = RtEvent::make_time_signature_event(11, {.numerator = 6, .denominator = 4});\n    event = Event::from_rt_event(time_sig_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_engine_notification());\n    auto time_sig_not = static_cast<TimeSignatureNotificationEvent*>(event.get());\n    EXPECT_TRUE(time_sig_not->is_time_sign_notification());\n    EXPECT_EQ(6, time_sig_not->time_signature().numerator);\n    EXPECT_EQ(4, time_sig_not->time_signature().denominator);\n\n    auto play_mode_event = RtEvent::make_playing_mode_event(12, PlayingMode::RECORDING);\n    event = Event::from_rt_event(play_mode_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_engine_notification());\n    auto play_mode_not = static_cast<PlayingModeNotificationEvent*>(event.get());\n    EXPECT_TRUE(play_mode_not->is_playing_mode_notification());\n    EXPECT_EQ(PlayingMode::RECORDING, play_mode_not->mode());\n\n    auto sync_mode_event = RtEvent::make_sync_mode_event(13, SyncMode::MIDI);\n    event = Event::from_rt_event(sync_mode_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_engine_notification());\n    auto sync_mode_not = static_cast<SyncModeNotificationEvent*>(event.get());\n    EXPECT_TRUE(sync_mode_not->is_sync_mode_notification());\n    EXPECT_EQ(SyncMode::MIDI, sync_mode_not->mode());\n\n    auto async_work_event = RtEvent::make_async_work_event(dummy_processor_callback, 10, nullptr);\n    event = Event::from_rt_event(async_work_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_async_work_event());\n    EXPECT_TRUE(event->process_asynchronously());\n\n    BlobData testdata = {0, nullptr};\n    auto async_blod_del_event = RtEvent::make_delete_blob_event(testdata);\n    event = Event::from_rt_event(async_blod_del_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_async_work_event());\n    EXPECT_TRUE(event->process_asynchronously());\n\n    auto notify_event = RtEvent::make_processor_notify_event(30, ProcessorNotifyRtEvent::Action::PARAMETER_UPDATE);\n    event = Event::from_rt_event(notify_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_engine_notification());\n    EXPECT_EQ(static_cast<AudioGraphNotificationEvent*>(event.get())->action(), AudioGraphNotificationEvent::Action::PROCESSOR_UPDATED);\n\n    auto tick_event = RtEvent::make_timing_tick_event(14, 12);\n    event = Event::from_rt_event(tick_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(event != nullptr);\n    EXPECT_TRUE(event->is_engine_notification());\n    auto tick_not = static_cast<EngineTimingTickNotificationEvent*>(event.get());\n    EXPECT_TRUE(tick_not->is_timing_tick_notification());\n    EXPECT_EQ(12, tick_not->tick_count());\n}\n"
  },
  {
    "path": "test/unittests/library/fixed_stack_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"library/fixed_stack.h\"\n\nnamespace sushi\n{\n\ntemplate<typename T, size_t storage_capacity>\nclass FixedStackAccessor\n{\npublic:\n    explicit FixedStackAccessor(FixedStack<T, storage_capacity>& f) : _friend(f) {}\n\n    const std::array<T, storage_capacity>& data()\n    {\n        return _friend._data;\n    }\n\nprivate:\n    FixedStack<T, storage_capacity>& _friend;\n};\n\n}\n\nusing namespace sushi;\n\nconstexpr int STACK_SIZE = 5;\n\nclass TestFixedStack : public ::testing::Test\n{\nprotected:\n    TestFixedStack() = default;\n\n    FixedStack<int, STACK_SIZE> _module_under_test;\n\n    FixedStackAccessor<int, STACK_SIZE> _accessor {_module_under_test};\n};\n\nTEST_F(TestFixedStack, TestPush)\n{\n    EXPECT_TRUE(_module_under_test.empty());\n\n    for (int i = 0; i < STACK_SIZE; i++)\n    {\n        EXPECT_TRUE(_module_under_test.push(i));\n        EXPECT_FALSE(_module_under_test.empty());\n    }\n    ASSERT_FALSE(_module_under_test.push(10));\n\n    ASSERT_EQ(2, _accessor.data()[2]);\n}\n\nTEST_F(TestFixedStack, TestPop)\n{\n    for (int i = 0; i < STACK_SIZE; i++)\n    {\n        EXPECT_TRUE(_module_under_test.push(i));\n    }\n    ASSERT_TRUE(_module_under_test.full());\n\n    int i = 0;\n    while (!_module_under_test.empty())\n    {\n        int val;\n        ASSERT_TRUE(_module_under_test.pop(val));\n        ASSERT_EQ(val, STACK_SIZE - i - 1);\n        i++;\n    }\n}\n\n"
  },
  {
    "path": "test/unittests/library/id_generator_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"library/id_generator.h\"\n\nTEST(BaseIdGeneratorTest, GenerateNewUid)\n{\n    ObjectId id_1 = BaseIdGenerator<ObjectId>::new_id();\n    ObjectId id_2 = BaseIdGenerator<ObjectId>::new_id();\n    ObjectId id_3 = BaseIdGenerator<ObjectId>::new_id();\n\n    /* Verify that generated ids are unique and consecutive */\n    EXPECT_NE(id_1, id_2);\n    EXPECT_EQ(id_2 + 1, id_3);\n\n    ObjectId p_id = ProcessorIdGenerator::new_id();\n\n    EXPECT_NE(id_1, p_id);\n}\n"
  },
  {
    "path": "test/unittests/library/internal_plugin_test.cpp",
    "content": "#include <tuple>\n\n#include \"gtest/gtest.h\"\n\n#include \"engine/transport.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"library/internal_plugin.cpp\"\n\n#include \"test_utils/host_control_mockup.h\"\n#include \"test_utils/test_utils.h\"\n\nnamespace sushi::internal\n{\n\nclass InternalPluginAccessor\n{\npublic:\n    explicit InternalPluginAccessor(InternalPlugin& plugin) : _plugin(plugin) {}\n\n    [[nodiscard]] std::deque<ParameterStorage>& parameter_values()\n    {\n        return _plugin._parameter_values;\n    }\n\n    void send_property_to_realtime(ObjectId property_id, const std::string& value)\n    {\n        _plugin.send_property_to_realtime(property_id, value);\n    }\n\n    void send_data_to_realtime(BlobData data, int id)\n    {\n        _plugin.send_data_to_realtime(data, id);\n    }\n\nprivate:\n    InternalPlugin& _plugin;\n};\n\n}\n\nusing namespace sushi;\nusing namespace sushi::internal;\n\nclass TestPlugin : public InternalPlugin\n{\npublic:\n    explicit TestPlugin(HostControl host_control) : InternalPlugin(host_control)\n    {\n        set_name(\"test_plugin\");\n    }\n\n    void process_audio(const ChunkSampleBuffer &in_buffer, ChunkSampleBuffer &out_buffer) override\n    {\n        out_buffer = in_buffer;\n    }\n};\n\n\nclass InternalPluginTest : public ::testing::Test\n{\nprotected:\n    InternalPluginTest() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<TestPlugin>(_host_control.make_host_control_mockup());\n        _module_under_test->set_event_output(&_host_control._event_output);\n\n        _accessor = std::make_unique<InternalPluginAccessor>(*_module_under_test);\n    }\n\n    HostControlMockup _host_control;\n\n    std::unique_ptr<InternalPlugin> _module_under_test;\n\n    std::unique_ptr<InternalPluginAccessor> _accessor;\n};\n\n\nTEST_F(InternalPluginTest, TestInstanciation)\n{\n    EXPECT_EQ(\"test_plugin\", _module_under_test->name());\n}\n\nTEST_F(InternalPluginTest, TestParameterRegistration)\n{\n    EXPECT_TRUE(_module_under_test->register_bool_parameter(\"bool\", \"Bool\", \"bool\", false, Direction::AUTOMATABLE));\n    EXPECT_TRUE(_module_under_test->register_property(\"string\", \"String\", \"default\"));\n    EXPECT_TRUE(_module_under_test->register_int_parameter(\"int\", \"Int\", \"numbers\",\n                                                           3, 0, 10,\n                                                           Direction::AUTOMATABLE,\n                                                           new IntParameterPreProcessor(0, 10)));\n\n    EXPECT_TRUE(_module_under_test->register_float_parameter(\"float\", \"Float\", \"fl\",\n                                                             5.0f, 0.0f, 10.0f,\n                                                             Direction::AUTOMATABLE,\n                                                             new FloatParameterPreProcessor(0.0, 10.0)));\n\n    // Verify all parameter/properties were registered and their order match\n    auto parameter_list = _module_under_test->all_parameters();\n    EXPECT_EQ(4u, parameter_list.size());\n\n    EXPECT_EQ(4u, _accessor->parameter_values().size());\n    IntParameterValue* value = nullptr;\n    ASSERT_NO_FATAL_FAILURE(value = _accessor->parameter_values()[2].int_parameter_value());\n    EXPECT_EQ(3, value->processed_value());\n}\n\nTEST_F(InternalPluginTest, TestDuplicateParameterNames)\n{\n    auto test_param = _module_under_test->register_int_parameter(\"param_2\", \"Param 2\", \"\",\n                                                                 1, 0, 10,\n                                                                 Direction::AUTOMATABLE,\n                                                                 new IntParameterPreProcessor(0, 10));\n    EXPECT_TRUE(test_param);\n    //  Register another parameter with the same name and assert that we get a null pointer back\n    auto test_param_2 = _module_under_test->register_bool_parameter(\"param_2\", \"Param 2\", \"\", false, Direction::AUTOMATABLE);\n    EXPECT_FALSE(test_param_2);\n}\n\nTEST_F(InternalPluginTest, TestBoolParameterHandling)\n{\n    BoolParameterValue* value = _module_under_test->register_bool_parameter(\"param_1\", \"Param 1\", \"\", false, Direction::AUTOMATABLE);\n    EXPECT_TRUE(value);\n\n    // Access the parameter through its id, verify type and that you can set its value.\n    EXPECT_EQ(ParameterType::BOOL, _module_under_test->parameter_from_name(\"param_1\")->type());\n    RtEvent event = RtEvent::make_parameter_change_event(0, 0, 0, 6.0f);\n    _module_under_test->process_event(event);\n    EXPECT_TRUE(value->processed_value());\n    // Access the parameter from the external interface\n    auto [status, ext_value] = _module_under_test->parameter_value(value->descriptor()->id());\n    EXPECT_EQ(ProcessorReturnCode::OK, status);\n    EXPECT_FLOAT_EQ(1.0f, ext_value);\n\n    auto [status_1, str_value] = _module_under_test->parameter_value_formatted(value->descriptor()->id());\n    EXPECT_EQ(ProcessorReturnCode::OK, status_1);\n    EXPECT_EQ(\"True\", str_value);\n\n    auto [err_status, unused_value] = _module_under_test->parameter_value(45);\n    EXPECT_EQ(ProcessorReturnCode::PARAMETER_NOT_FOUND, err_status);\n\n    DECLARE_UNUSED(unused_value);\n}\n\nTEST_F(InternalPluginTest, TestIntParameterHandling)\n{\n    auto value = _module_under_test->register_int_parameter(\"param_1\", \"Param 1\", \"\",\n                                                            0, 0, 10,\n                                                            Direction::AUTOMATABLE,\n                                                            new IntParameterPreProcessor(0, 10));\n    EXPECT_TRUE(value);\n\n    // Access the parameter through its id, verify type and that you can set its value.\n    EXPECT_EQ(ParameterType::INT, _module_under_test->parameter_from_name(\"param_1\")->type());\n\n    RtEvent event = RtEvent::make_parameter_change_event(0, 0, 0, 0.6f);\n\n    _module_under_test->process_event(event);\n\n    EXPECT_FLOAT_EQ(6.0f, static_cast<float>(value->processed_value()));\n\n    // Access the parameter from the external interface\n    auto [status, ext_value] = _module_under_test->parameter_value_in_domain(value->descriptor()->id());\n    EXPECT_EQ(ProcessorReturnCode::OK, status);\n    EXPECT_FLOAT_EQ(6.0f, ext_value);\n\n    auto [status_1, norm_value] = _module_under_test->parameter_value(value->descriptor()->id());\n    EXPECT_EQ(ProcessorReturnCode::OK, status_1);\n    EXPECT_FLOAT_EQ(0.6f, norm_value);\n\n    auto [status_2, str_value] = _module_under_test->parameter_value_formatted(value->descriptor()->id());\n    EXPECT_EQ(ProcessorReturnCode::OK, status_2);\n    EXPECT_EQ(\"6\", str_value);\n\n    auto [err_status, unused_value] = _module_under_test->parameter_value(45);\n    EXPECT_EQ(ProcessorReturnCode::PARAMETER_NOT_FOUND, err_status);\n\n\n\n    DECLARE_UNUSED(unused_value);\n}\n\nTEST_F(InternalPluginTest, TestFloatParameterHandling)\n{\n    auto value = _module_under_test->register_float_parameter(\"param_1\", \"Param 1\", \"\",\n                                                              1.0f, 0.0f, 10.f,\n                                                              Direction::AUTOMATABLE,\n                                                              new FloatParameterPreProcessor(0.0f, 10.0f));\n    EXPECT_TRUE(value);\n\n    // Access the parameter through its id, verify type and that you can set its value.\n    EXPECT_EQ(ParameterType::FLOAT, _module_under_test->parameter_from_name(\"param_1\")->type());\n\n    RtEvent event = RtEvent::make_parameter_change_event(0, 0, 0, 0.5f);\n    _module_under_test->process_event(event);\n    EXPECT_EQ(5, value->processed_value());\n\n    // Access the parameter from the external interface\n    auto [status, ext_value] = _module_under_test->parameter_value_in_domain(value->descriptor()->id());\n    EXPECT_EQ(ProcessorReturnCode::OK, status);\n    EXPECT_FLOAT_EQ(5.0f, ext_value);\n\n    auto [status_1, norm_value] = _module_under_test->parameter_value(value->descriptor()->id());\n    EXPECT_EQ(ProcessorReturnCode::OK, status_1);\n    EXPECT_FLOAT_EQ(0.5f, norm_value);\n\n    auto [status_2, str_value] = _module_under_test->parameter_value_formatted(value->descriptor()->id());\n    EXPECT_EQ(ProcessorReturnCode::OK, status_2);\n    EXPECT_EQ(\"5.00\", str_value);\n\n    [[maybe_unused]] auto [err_status, unused_value] = _module_under_test->parameter_value(45);\n    EXPECT_EQ(ProcessorReturnCode::PARAMETER_NOT_FOUND, err_status);\n\n    DECLARE_UNUSED(unused_value);\n}\n\nTEST_F(InternalPluginTest, TestPropertyHandling)\n{\n    auto descriptor = _module_under_test->register_property(\"str_1\", \"Str_1\", \"test\");\n    ASSERT_TRUE(descriptor);\n\n    // Access the property through its id, verify type and that you can set its value.\n    auto param = _module_under_test->parameter_from_name(\"str_1\");\n    ASSERT_TRUE(param);\n    EXPECT_EQ(ParameterType::STRING, param->type());\n\n    // String properties are set directly in a non-rt thread.\n    EXPECT_EQ(\"test\", _module_under_test->property_value(param->id()).second);\n    EXPECT_NE(ProcessorReturnCode::OK, _module_under_test->property_value(12345).first);\n\n    EXPECT_EQ(ProcessorReturnCode::OK, _module_under_test->set_property_value(param->id(), \"updated\"));\n    EXPECT_EQ(\"updated\", _module_under_test->property_value(param->id()).second);\n\n    EXPECT_NE(ProcessorReturnCode::OK, _module_under_test->set_property_value(12345, \"no_property\"));\n}\n\nTEST_F(InternalPluginTest, TestSendingPropertyToRealtime)\n{\n    _module_under_test->register_property(\"property\", \"Property\", \"default\");\n    _accessor->send_property_to_realtime(0, \"test\");\n\n    // Check that an event was generated and queued\n    auto event = _host_control._dummy_dispatcher.retrieve_event();\n    ASSERT_TRUE(event.operator bool());\n    EXPECT_TRUE(event->maps_to_rt_event());\n    auto rt_event = event->to_rt_event(0);\n    EXPECT_EQ(RtEventType::STRING_PROPERTY_CHANGE, rt_event.type());\n\n    // Pass the RtEvent to the plugin, and verify that a delete event was generated in response\n    _module_under_test->process_event(rt_event);\n    RtEvent response_event;\n    ASSERT_TRUE(_host_control._event_output.pop(response_event));\n    EXPECT_EQ(RtEventType::DELETE, response_event.type());\n\n    // Delete the string manually, otherwise done by the dispatcher.\n    delete response_event.delete_data_event()->data();\n}\n\nTEST_F(InternalPluginTest, TestSendingDataToRealtime)\n{\n    int a = 123;\n    BlobData data{.size = sizeof(a), .data = (uint8_t*) &a};\n    _accessor->send_data_to_realtime(data, 15);\n\n    // Check that an event was generated and queued\n    auto event = _host_control._dummy_dispatcher.retrieve_event();\n    ASSERT_TRUE(event.operator bool());\n    EXPECT_TRUE(event->maps_to_rt_event());\n    auto rt_event = event->to_rt_event(0);\n    EXPECT_EQ(RtEventType::DATA_PROPERTY_CHANGE, rt_event.type());\n\n    EXPECT_EQ(123, *rt_event.data_parameter_change_event()->value().data);\n}\n\nTEST_F(InternalPluginTest, TestStateHandling)\n{\n    auto parameter = _module_under_test->register_float_parameter(\"param_1\", \"Param 1\", \"\",\n                                                                  1.0f, 0.0f, 10.f,\n                                                                  Direction::AUTOMATABLE,\n                                                                  new FloatParameterPreProcessor(0.0f, 10.0f));\n    ASSERT_TRUE(parameter);\n    auto property = _module_under_test->register_property(\"str_1\", \"Str_1\", \"test\");\n    ASSERT_TRUE(property);\n    auto descriptor = _module_under_test->parameter_from_name(\"str_1\");\n\n    ProcessorState state;\n    state.set_bypass(true);\n    state.add_parameter_change(parameter->descriptor()->id(), 0.25);\n    state.add_property_change(descriptor->id(), \"new_value\");\n\n    auto status = _module_under_test->set_state(&state, false);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    // Check that new values are set\n    EXPECT_FLOAT_EQ(0.25f, _module_under_test->parameter_value(parameter->descriptor()->id()).second);\n    EXPECT_EQ(\"new_value\", _module_under_test->property_value(descriptor->id()).second);\n    EXPECT_TRUE(_module_under_test->bypassed());\n}\n\nTEST_F(InternalPluginTest, TestRtStateHandling)\n{\n    auto parameter = _module_under_test->register_float_parameter(\"param_1\", \"Param 1\", \"\",\n                                                                  10.0f, 0.0f, 10.f,\n                                                                  Direction::AUTOMATABLE,\n                                                                  new FloatParameterPreProcessor(0.0f, 10.0f));\n    ASSERT_TRUE(parameter);\n\n    ProcessorState state;\n    state.set_bypass(true);\n    state.add_parameter_change(parameter->descriptor()->id(), 0.25);\n\n    auto status = _module_under_test->set_state(&state, true);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    // Values should not be changed yet\n    EXPECT_FLOAT_EQ(1.0f, _module_under_test->parameter_value(parameter->descriptor()->id()).second);\n    EXPECT_FALSE(_module_under_test->bypassed());\n\n    // Plugin should generate a request to send an RtEvent to the plugin\n    auto event = _host_control._dummy_dispatcher.retrieve_event();\n    ASSERT_TRUE(event.get());\n    auto rt_event = event->to_rt_event(0);\n    _module_under_test->process_event(rt_event);\n\n    // Now the values should have changed\n    EXPECT_FLOAT_EQ(0.25f, _module_under_test->parameter_value(parameter->descriptor()->id()).second);\n    EXPECT_TRUE(_module_under_test->bypassed());\n\n    // Retrieve the delete event and execute it\n    ASSERT_FALSE(_host_control._event_output.empty());\n\n    EXPECT_EQ(_host_control._event_output.size(), 2);\n\n    while (!_host_control._event_output.empty())\n    {\n        rt_event = _host_control._event_output.pop();\n        auto from_rt_event = Event::from_rt_event(rt_event, IMMEDIATE_PROCESS);\n        ASSERT_TRUE(from_rt_event);\n\n        if (rt_event.type() == RtEventType::DELETE)\n        {\n            static_cast<AsynchronousDeleteEvent*>(from_rt_event.get())->execute();\n        }\n        else if (rt_event.type() == RtEventType::NOTIFY)\n        {\n            auto cast_event = static_cast<AudioGraphNotificationEvent*>(from_rt_event.get());\n            EXPECT_EQ(cast_event->action(), AudioGraphNotificationEvent::Action::PROCESSOR_UPDATED);\n        }\n    }\n}\n\nTEST_F(InternalPluginTest, TestStateSaving)\n{\n    ChunkSampleBuffer buffer;\n    auto parameter = _module_under_test->register_float_parameter(\"param_1\", \"Param 1\", \"\",\n                                                                  1.0f, 0.0f, 10.f,\n                                                                  Direction::AUTOMATABLE,\n                                                                  new FloatParameterPreProcessor(0.0f, 10.0f));\n    ASSERT_TRUE(parameter);\n    ObjectId param_id = parameter->descriptor()->id();\n    auto property = _module_under_test->register_property(\"str_1\", \"Str_1\", \"test\");\n    ASSERT_TRUE(property);\n    auto descriptor = _module_under_test->parameter_from_name(\"str_1\");\n    float param_val = _module_under_test->parameter_value(param_id).second;\n    std::string str_val = _module_under_test->property_value(descriptor->id()).second;\n\n    ProcessorState state = _module_under_test->save_state();\n\n    _module_under_test->set_property_value(descriptor->id(), \"str_2\");\n    auto rt_event = RtEvent::make_parameter_change_event(0, 0, param_id, 0.4f);\n    _module_under_test->process_event(rt_event);\n\n    ASSERT_NE(param_val, _module_under_test->parameter_value(param_id).second);\n    ASSERT_NE(str_val, _module_under_test->property_value(descriptor->id()).second);\n\n    auto status = _module_under_test->set_state(&state, false);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    // Check that values are restored\n    ASSERT_EQ(param_val, _module_under_test->parameter_value(parameter->descriptor()->id()).second);\n    ASSERT_EQ(str_val, _module_under_test->property_value(descriptor->id()).second);\n}\n\nTEST_F(InternalPluginTest, TestKeyboardEventPassthrough)\n{\n    _module_under_test->process_event(RtEvent::make_note_on_event(0, 0, 1, 28, 0.5f));\n    ASSERT_EQ(1u, _host_control._event_output.size());\n    ASSERT_EQ(RtEventType::NOTE_ON, _host_control._event_output.pop().type());\n\n    // Non-keyboard events should not pass through\n    _module_under_test->process_event(RtEvent::make_cv_event(0, 0, 1, 0.5f));\n    ASSERT_TRUE(_host_control._event_output.empty());\n}"
  },
  {
    "path": "test/unittests/library/lv2_wrapper_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/host_control_mockup.h\"\n\n#include \"test_utils/engine_mockup.h\"\n#include \"library/lv2/lv2_state.cpp\"\n\n#include \"spdlog/fmt/bundled/core.h\"\n#include \"spdlog/fmt/bundled/format.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_KEYWORD_MACRO\n#define private public\nELK_POP_WARNING\n\n#include \"library/lv2/lv2_wrapper.cpp\"\n\n#include \"library/lv2/lv2_port.cpp\"\n#include \"library/lv2/lv2_model.cpp\"\n#include \"library/lv2/lv2_worker.cpp\"\n#include \"library/lv2/lv2_control.cpp\"\n\n// For testing the LV2Log feature, override the logger macro to write to a local variable instead\n#undef ELKLOG_LOG_DEBUG\nstd::array<char, 1024> log_buffer;\n#define ELKLOG_LOG_DEBUG(fmt_str, ...) ::fmt::format_to(log_buffer.data(), fmt_str, __VA_ARGS__);\n#include \"library/lv2/lv2_features.cpp\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::lv2;\n\nconstexpr float TEST_SAMPLE_RATE = 48000;\nconstexpr int   TEST_CHANNEL_COUNT = 2;\n\n// Tip: use 'test_utils::print_buffer<64>(out_buffer, 1)'\n// to generate static buffer content in text like the below.\nstatic const float LV2_SAMPLER_EXPECTED_OUT_NOTE_ON[1][64] = {\n    {\n        8.5811443627e-02f, 1.5257082880e-01f, 1.9981867075e-01f, 2.6550742984e-01f,\n        3.1894043088e-01f, 3.6774355173e-01f, 4.2173218727e-01f, 4.7939392924e-01f,\n        5.2299284935e-01f, 5.6986409426e-01f, 6.1811023951e-01f, 6.6184180975e-01f,\n        7.0369291306e-01f, 7.4014079571e-01f, 7.8293782473e-01f, 8.1396549940e-01f,\n        8.4733998775e-01f, 8.7596982718e-01f, 8.9703381062e-01f, 9.2847150564e-01f,\n        9.4153606892e-01f, 9.6092289686e-01f, 9.7848141193e-01f, 9.7645491362e-01f,\n        9.9383544922e-01f, 9.9207293987e-01f, 9.8401486874e-01f, 9.9248045683e-01f,\n        9.8064988852e-01f, 9.7262036800e-01f, 9.5535176992e-01f, 9.4097930193e-01f,\n        9.2831599712e-01f, 8.9614868164e-01f, 8.7789064646e-01f, 8.4527707100e-01f,\n        8.1868839264e-01f, 7.8956091404e-01f, 7.3800379038e-01f, 7.0913290977e-01f,\n        6.6314095259e-01f, 6.1984896660e-01f, 5.8228880167e-01f, 5.2198934555e-01f,\n        4.8422691226e-01f, 4.2829886079e-01f, 3.6879178882e-01f, 3.2674816251e-01f,\n        2.6642197371e-01f, 2.1852211654e-01f, 1.5994016826e-01f, 1.0220003873e-01f,\n        4.7169614583e-02f, -2.0063044503e-02f, -7.0752970874e-02f, -1.3219092786e-01f,\n        -1.8827511370e-01f, -2.4446520209e-01f, -3.0550417304e-01f, -3.5686719418e-01f,\n        -4.1207060218e-01f, -4.5643404126e-01f, -5.0429958105e-01f, -5.5731260777e-01f\n    }\n};\n\nstatic const float LV2_SAMPLER_EXPECTED_OUT_NOTE_OFF[1][64] = {\n    {\n        -6.0585808754e-01f, -6.5102791786e-01f, -6.9954341650e-01f, -7.3845398426e-01f,\n        -7.6650136709e-01f, -8.0487918854e-01f, -8.4129923582e-01f, -8.7295645475e-01f,\n        -8.9587861300e-01f, -9.3141978979e-01f, -9.5426052809e-01f, -9.5808660984e-01f,\n        -9.8191249371e-01f, -9.9042803049e-01f, -9.9751955271e-01f, -1.0013926029e+00f,\n        -9.9870741367e-01f, -1.0012304783e+00f, -9.9909341335e-01f, -9.9926203489e-01f,\n        -9.7940754890e-01f, -9.6884649992e-01f, -9.5232754946e-01f, -9.2335337400e-01f,\n        -9.1003942490e-01f, -8.7584549189e-01f, -8.5085171461e-01f, -8.1899070740e-01f,\n        -7.7134633064e-01f, -7.4802970886e-01f, -7.0600986481e-01f, -6.6105115414e-01f,\n        -6.2150335312e-01f, -5.6417763233e-01f, -5.2068632841e-01f, -4.6517598629e-01f,\n        -4.1045567393e-01f, -3.6629143357e-01f, -3.0408981442e-01f, -2.5660526752e-01f,\n        -1.9908088446e-01f, -1.4596894383e-01f, -9.3292877078e-02f, -2.3014366627e-02f,\n        2.4260010570e-02f, 8.2992948592e-02f, 1.4165890217e-01f, 1.9666172564e-01f,\n        2.5683587790e-01f, 3.1207546592e-01f, 3.6573141813e-01f, 4.1807574034e-01f,\n        4.7608512640e-01f, 5.1777309179e-01f, 5.6559085846e-01f, 6.1442857981e-01f,\n        6.5522098541e-01f, 7.0046389103e-01f, 7.4100756645e-01f, 7.7862900496e-01f,\n        8.1257081032e-01f, 8.4414005280e-01f, 8.7060534954e-01f, 8.9672446251e-01f\n    }\n};\n\nclass TestLv2Wrapper : public ::testing::Test\n{\nprotected:\n    using ::testing::Test::SetUp; // Hide error of hidden overload of virtual function in clang when signatures differ but the name is the same\n    TestLv2Wrapper()\n    {}\n\n    ProcessorReturnCode SetUp(const std::string& plugin_URI)\n    {\n        auto mockup = _host_control.make_host_control_mockup(TEST_SAMPLE_RATE);\n        _host_control._transport.set_time(Time(0), 0);\n        _world = std::make_shared<LilvWorldWrapper>();\n        bool world_created = _world->create_world();\n        EXPECT_TRUE(world_created);\n        _module_under_test = std::make_unique<lv2::LV2_Wrapper>(mockup, plugin_URI, _world);\n\n        auto ret = _module_under_test->init(TEST_SAMPLE_RATE);\n\n        if (ret != ProcessorReturnCode::OK)\n        {\n            _module_under_test = nullptr;\n        }\n        else\n        {\n            _module_under_test->set_event_output(&_fifo);\n            _module_under_test->set_enabled(true);\n            _module_under_test->set_channels(std::min(TEST_CHANNEL_COUNT, _module_under_test->max_input_channels()),\n                                             std::min(TEST_CHANNEL_COUNT, _module_under_test->max_output_channels()));\n        }\n\n        return ret;\n    }\n\n    void TearDown()\n    {\n        _module_under_test.reset();\n    }\n\n    RtSafeRtEventFifo _fifo;\n\n\n\n    HostControlMockup _host_control;\n    std::shared_ptr<LilvWorldWrapper> _world;\n    std::unique_ptr<LV2_Wrapper> _module_under_test;\n};\n\nTEST_F(TestLv2Wrapper, TestLV2PluginInvalidURI)\n{\n    // Closing stderr causes a crash on Windows when code later tries to output to it.\n#ifndef _MSC_VER\n    // Closing console error output temporarily.\n    // Lilv complains the URI is invalid, as it should.\n    // We don't need that to pollute our unit test output.\n    fclose(stderr);\n#endif\n\n    auto ret = SetUp(\"This URI surely does not exist.\");\n\n    // And re-opening the output:\n    [[maybe_unused]] auto unused = freopen(\"/dev/tty\", \"w\", stderr);\n\n    ASSERT_EQ(ProcessorReturnCode::SHARED_LIBRARY_OPENING_ERROR, ret);\n    ASSERT_EQ(_module_under_test, nullptr);\n}\n\nTEST_F(TestLv2Wrapper, TestLV2PluginInteraction)\n{\n    auto ret = SetUp(\"http://lv2plug.in/plugins/eg-amp\");\n    ASSERT_EQ(ProcessorReturnCode::OK, ret);\n\n    int pCount = _module_under_test->parameter_count();\n    EXPECT_EQ(1, pCount);\n    auto param0 = _module_under_test->parameter_from_id(0);\n    EXPECT_TRUE(param0);\n    EXPECT_EQ(0u, param0->id());\n\n    auto paramNull = _module_under_test->parameter_from_id(1);\n    EXPECT_EQ(paramNull, nullptr);\n\n    // TestSetName\n    EXPECT_EQ(\"http://lv2plug.in/plugins/eg-amp\", _module_under_test->name());\n    EXPECT_EQ(\"Simple Amplifier\", _module_under_test->label());\n\n    // TestParameterInitialization\n    auto gain_param = _module_under_test->parameter_from_name(\"Gain\");\n    EXPECT_TRUE(gain_param);\n    EXPECT_EQ(0u, gain_param->id());\n\n    // TestParameterSetViaEvent\n    auto parameter_change_event = RtEvent::make_parameter_change_event(0, 0, 0, 0.5f);\n    _module_under_test->process_event(parameter_change_event);\n    auto value = _module_under_test->parameter_value(0);\n    EXPECT_EQ(0.5f, value.second);\n\n    // TestFetchingFormattedParameterValue\n    auto [status, formattedValue] = _module_under_test->parameter_value_formatted(0);\n    EXPECT_EQ(ProcessorReturnCode::OK, status);\n    EXPECT_EQ(\"-33.000000\", formattedValue);\n}\n\nTEST_F(TestLv2Wrapper, TestProcessingWithParameterChanges)\n{\n    auto ret = SetUp(\"http://lv2plug.in/plugins/eg-amp\");\n    ASSERT_EQ(ProcessorReturnCode::OK, ret);\n\n    ChunkSampleBuffer in_buffer(1);\n    ChunkSampleBuffer out_buffer(1);\n\n    // TestProcessingWithParameterChanges\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(1.0f, out_buffer);\n\n    // Verify that a parameter change affects the sound.\n    // eg-amp plugins Gain parameter range is from -90 to 24\n    auto lower_gain_Event = RtEvent::make_parameter_change_event(0, 0, 0, 0.0f);\n    _module_under_test->process_event(lower_gain_Event);\n\n    _module_under_test->process_audio(in_buffer, out_buffer);\n\n    test_utils::assert_buffer_value(0.0f, out_buffer);\n\n    auto [status, parameter_value] = _module_under_test->parameter_value(0);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    EXPECT_EQ(0.0f, parameter_value);\n}\n\nTEST_F(TestLv2Wrapper, TestBypassProcessing)\n{\n    auto ret = SetUp(\"http://lv2plug.in/plugins/eg-amp\");\n    ASSERT_EQ(ProcessorReturnCode::OK, ret);\n\n    ChunkSampleBuffer in_buffer(1);\n    ChunkSampleBuffer out_buffer(1);\n    auto event = RtEvent::make_parameter_change_event(0, 0, 0, 0.3f);\n    _module_under_test->process_event(event);\n\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n\n    // Set bypass and manually feed the generated RtEvent back to the\n    // wrapper processor as event dispatcher is not running\n    _module_under_test->set_bypassed(true);\n    auto bypass_event = _host_control._dummy_dispatcher.retrieve_event();\n    EXPECT_TRUE(bypass_event.get());\n\n    _module_under_test->process_event(bypass_event->to_rt_event(0));\n    EXPECT_TRUE(_module_under_test->bypassed());\n\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    // Test that we are ramping up the audio to the bypass value\n    float prev_value = 0;\n    for (int i = 1; i < AUDIO_CHUNK_SIZE; ++i)\n    {\n        EXPECT_GT(out_buffer.channel(0)[i], prev_value);\n        prev_value = out_buffer.channel(0)[i];\n    }\n}\n\nTEST_F(TestLv2Wrapper, TestMidiEventInputAndOutput)\n{\n    // Use a plugin that doubles midi notes a fifth up for testing\n    auto ret = SetUp(\"http://lv2plug.in/plugins/eg-fifths\");\n    ASSERT_EQ(ProcessorReturnCode::OK, ret);\n\n    ASSERT_TRUE(_fifo.empty());\n\n    ChunkSampleBuffer in_buffer(2);\n    ChunkSampleBuffer out_buffer(2);\n\n    _module_under_test->process_event(RtEvent::make_note_on_event(0, 0, 1, 60, 1.0f));\n    _module_under_test->process_event(RtEvent::make_note_off_event(0, 0, 2, 60, 0.5f));\n    _module_under_test->process_audio(in_buffer, out_buffer);\n\n    RtEvent e;\n    ASSERT_TRUE(_fifo.pop(e));\n    EXPECT_EQ(_module_under_test->id(), e.processor_id());\n\n    EXPECT_EQ(RtEventType::NOTE_ON, e.type());\n    EXPECT_EQ(1, e.keyboard_event()->channel());\n    EXPECT_EQ(60, e.keyboard_event()->note());\n    EXPECT_FLOAT_EQ(1.0f, e.keyboard_event()->velocity());\n\n    ASSERT_TRUE(_fifo.pop(e));\n    EXPECT_EQ(RtEventType::NOTE_ON, e.type());\n    EXPECT_EQ(1, e.keyboard_event()->channel());\n    EXPECT_EQ(67, e.keyboard_event()->note());\n    EXPECT_FLOAT_EQ(1.0f, e.keyboard_event()->velocity());\n\n    ASSERT_TRUE(_fifo.pop(e));\n    EXPECT_EQ(RtEventType::NOTE_OFF, e.type());\n    EXPECT_EQ(2, e.keyboard_event()->channel());\n    EXPECT_EQ(60, e.keyboard_event()->note());\n    EXPECT_NEAR(0.5f, e.keyboard_event()->velocity(), 0.01);\n\n    ASSERT_TRUE(_fifo.pop(e));\n    EXPECT_EQ(RtEventType::NOTE_OFF, e.type());\n    EXPECT_EQ(2, e.keyboard_event()->channel());\n    EXPECT_EQ(67, e.keyboard_event()->note());\n    EXPECT_NEAR(0.5f, e.keyboard_event()->velocity(), 0.01);\n\n    ASSERT_TRUE(_fifo.empty());\n}\n\n/* TODO: Currently incomplete.\n * Complete this with fetching of transport info from LV2 plugin, to compare states.\n * As it is now, at least it demonstrates that an LV2 plugin that requires the 'time' extension,\n * successfully loads.\n*/\nTEST_F(TestLv2Wrapper, TestTimeInfo)\n{\n    auto ret = SetUp(\"http://lv2plug.in/plugins/eg-metro\");\n    ASSERT_EQ(ProcessorReturnCode::OK, ret);\n\n    _host_control._transport.set_tempo(60, false);\n    _host_control._transport.set_time_signature({4, 4}, false);\n    _host_control._transport.set_time(std::chrono::seconds(1), static_cast<int64_t>(TEST_SAMPLE_RATE));\n\n    // Currently, The Sushi LV2 hosting does not get time info from plugin - it only sets.\n    // So we cannot directly replicate the below.\n    /*\n    auto time_info = _module_under_test->time_info();\n    EXPECT_EQ(static_cast<int64_t>(TEST_SAMPLE_RATE), time_info->samplePos);\n    EXPECT_EQ(1'000'000'000, time_info->nanoSeconds);\n    EXPECT_FLOAT_EQ(1.0f, time_info->ppqPos);\n    EXPECT_FLOAT_EQ(60.0f, time_info->tempo);\n    EXPECT_FLOAT_EQ(0.0f, time_info->barStartPos);\n    EXPECT_EQ(4, time_info->timeSigNumerator);\n    EXPECT_EQ(4, time_info->timeSigDenominator);\n     */\n}\n\n\n#ifndef DISABLE_MULTICORE_UNIT_TESTS\n// TODO: This tests synchronous worker invocation.\n// Asynchronous invocation requires an additional LV2 extension to be implemented first,\n// optional for eg-sampler, but required for its sample-loading feature.\nTEST_F(TestLv2Wrapper, TestSynchronousStateAndWorkerThreads)\n{\n    auto ret = SetUp(\"http://lv2plug.in/plugins/eg-sampler\");\n    ASSERT_EQ(ProcessorReturnCode::OK, ret);\n\n    ChunkSampleBuffer in_buffer(1);\n    ChunkSampleBuffer out_buffer(1);\n\n    _module_under_test->process_event(RtEvent::make_note_on_event(0, 0, 0, 60, 1.0f));\n    _module_under_test->process_audio(in_buffer, out_buffer);\n\n    // Increment channels to 2 when supporting stereo loading of mono plugins.\n    test_utils::compare_buffers(LV2_SAMPLER_EXPECTED_OUT_NOTE_ON, out_buffer, 1, 0.0001f);\n\n    _module_under_test->process_event(RtEvent::make_note_off_event(0, 0, 0, 60, 1.0f));\n    _module_under_test->process_audio(in_buffer, out_buffer);\n\n    if (AUDIO_CHUNK_SIZE == 64)\n    {\n        // Increment channels to 2 when supporting stereo loading of mono plugins.\n        test_utils::compare_buffers(LV2_SAMPLER_EXPECTED_OUT_NOTE_OFF, out_buffer, 1, 0.0001f);\n    }\n    else\n    {\n        std::cout << \"AUDIO_CHUNK_SIZE != 64 - audio buffer comparisons after NOTE_OFF events in LV2 tests cannot run\"\n                  << std::endl;\n    }\n}\n#endif\n\n#ifdef SUSHI_BUILD_WITH_LV2_MDA_TESTS\n\nstatic const float LV2_JX10_EXPECTED_OUT_NOTE_ON[2][64] = {\n    {\n        0.0000000000e+00f, -1.3231920004e-09f, -5.8071242259e-11f, 7.4176806919e-09f,\n        1.6889693200e-08f, 2.0939033618e-08f, 6.3323604138e-09f, -4.3704385888e-08f,\n        -1.5136777165e-07f, -3.4226587786e-07f, -6.4724713411e-07f, -1.1003317013e-06f,\n        -1.7406666757e-06f, -2.6102886750e-06f, -3.7562799662e-06f, -5.2283103287e-06f,\n        -7.0810297075e-06f, -9.3713651950e-06f, -1.2161169252e-05f, -1.5514257029e-05f,\n        -1.9499317204e-05f, -2.4186683731e-05f, -2.9651528166e-05f, -3.5970344470e-05f,\n        -4.3224412366e-05f, -5.1496041124e-05f, -6.0872265749e-05f, -7.1440852480e-05f,\n        -8.3294245997e-05f, -9.6525320259e-05f, -1.1123159493e-04f, -1.2751069153e-04f,\n        -1.4546485909e-04f, -1.6561846132e-04f, -1.8773633929e-04f, -2.1193045541e-04f,\n        -2.3831747239e-04f, -2.6701329625e-04f, -2.9813844594e-04f, -3.3181239269e-04f,\n        -3.6815920612e-04f, -4.0730155888e-04f, -4.4936660561e-04f, -4.9448001664e-04f,\n        -5.4277182790e-04f, -5.9437012533e-04f, -6.4940738957e-04f, -7.0801418042e-04f,\n        -7.7032501576e-04f, -8.3647237625e-04f, -9.0659258422e-04f, -9.8081969190e-04f,\n        -1.0592915351e-03f, -1.1421436211e-03f, -1.2295149500e-03f, -1.3215418439e-03f,\n        -1.4183644671e-03f, -1.5201196074e-03f, -1.6269480111e-03f, -1.7389869317e-03f,\n        -1.8563776975e-03f, -1.9792574458e-03f, -2.1077671554e-03f, -2.2420443129e-03f\n    },\n    {\n        0.0000000000e+00f, -1.3231920004e-09f, -5.8071242259e-11f, 7.4176806919e-09f,\n        1.6889693200e-08f, 2.0939033618e-08f, 6.3323604138e-09f, -4.3704385888e-08f,\n        -1.5136777165e-07f, -3.4226587786e-07f, -6.4724713411e-07f, -1.1003317013e-06f,\n        -1.7406666757e-06f, -2.6102886750e-06f, -3.7562799662e-06f, -5.2283103287e-06f,\n        -7.0810297075e-06f, -9.3713651950e-06f, -1.2161169252e-05f, -1.5514257029e-05f,\n        -1.9499317204e-05f, -2.4186683731e-05f, -2.9651528166e-05f, -3.5970344470e-05f,\n        -4.3224412366e-05f, -5.1496041124e-05f, -6.0872265749e-05f, -7.1440852480e-05f,\n        -8.3294245997e-05f, -9.6525320259e-05f, -1.1123159493e-04f, -1.2751069153e-04f,\n        -1.4546485909e-04f, -1.6561846132e-04f, -1.8773633929e-04f, -2.1193045541e-04f,\n        -2.3831747239e-04f, -2.6701329625e-04f, -2.9813844594e-04f, -3.3181239269e-04f,\n        -3.6815920612e-04f, -4.0730155888e-04f, -4.4936660561e-04f, -4.9448001664e-04f,\n        -5.4277182790e-04f, -5.9437012533e-04f, -6.4940738957e-04f, -7.0801418042e-04f,\n        -7.7032501576e-04f, -8.3647237625e-04f, -9.0659258422e-04f, -9.8081969190e-04f,\n        -1.0592915351e-03f, -1.1421436211e-03f, -1.2295149500e-03f, -1.3215418439e-03f,\n        -1.4183644671e-03f, -1.5201196074e-03f, -1.6269480111e-03f, -1.7389869317e-03f,\n        -1.8563776975e-03f, -1.9792574458e-03f, -2.1077671554e-03f, -2.2420443129e-03f\n    }\n};\n\nstatic const float LV2_JX10_EXPECTED_OUT_NOTE_OFF[2][64] = {\n    {\n        -2.3517450318e-03f, -2.4647361133e-03f, -2.5843831245e-03f, -2.7075796388e-03f,\n        -2.8343601152e-03f, -2.9647541232e-03f, -3.0987935606e-03f, -3.2365065999e-03f,\n        -3.3779235091e-03f, -3.5230703652e-03f, -3.6719755735e-03f, -3.8246638142e-03f,\n        -3.9811609313e-03f, -4.1414904408e-03f, -4.3056765571e-03f, -4.4737402350e-03f,\n        -4.6457038261e-03f, -4.8215868883e-03f, -5.0014094450e-03f, -5.1851901226e-03f,\n        -5.3729466163e-03f, -5.5646947585e-03f, -5.7604517788e-03f, -5.9602307156e-03f,\n        -6.1640464701e-03f, -6.3719111495e-03f, -6.5838382579e-03f, -6.7998361774e-03f,\n        -7.0199179463e-03f, -7.2440896183e-03f, -7.4723623693e-03f, -7.7047408558e-03f,\n        -7.9412339255e-03f, -8.1818439066e-03f, -8.4265777841e-03f, -8.6825294420e-03f,\n        -8.9428499341e-03f, -9.2075373977e-03f, -9.4765927643e-03f, -9.7500113770e-03f,\n        -1.0027793236e-02f, -1.0309931822e-02f, -1.0596422479e-02f, -1.0887259617e-02f,\n        -1.1182436720e-02f, -1.1481943540e-02f, -1.1785773560e-02f, -1.2093913741e-02f,\n        -1.2406354770e-02f, -1.2723082677e-02f, -1.3044086285e-02f, -1.3369349763e-02f,\n        -1.3698859140e-02f, -1.4032597654e-02f, -1.4370549470e-02f, -1.4712693170e-02f,\n        -1.5059012920e-02f, -1.5409486368e-02f, -1.5764094889e-02f, -1.6122814268e-02f,\n        -1.6485624015e-02f, -1.6852496192e-02f, -1.7223412171e-02f, -1.7598342150e-02f\n    },\n    {\n        -2.3517450318e-03f, -2.4647361133e-03f, -2.5843831245e-03f, -2.7075796388e-03f,\n        -2.8343601152e-03f, -2.9647541232e-03f, -3.0987935606e-03f, -3.2365065999e-03f,\n        -3.3779235091e-03f, -3.5230703652e-03f, -3.6719755735e-03f, -3.8246638142e-03f,\n        -3.9811609313e-03f, -4.1414904408e-03f, -4.3056765571e-03f, -4.4737402350e-03f,\n        -4.6457038261e-03f, -4.8215868883e-03f, -5.0014094450e-03f, -5.1851901226e-03f,\n        -5.3729466163e-03f, -5.5646947585e-03f, -5.7604517788e-03f, -5.9602307156e-03f,\n        -6.1640464701e-03f, -6.3719111495e-03f, -6.5838382579e-03f, -6.7998361774e-03f,\n        -7.0199179463e-03f, -7.2440896183e-03f, -7.4723623693e-03f, -7.7047408558e-03f,\n        -7.9412339255e-03f, -8.1818439066e-03f, -8.4265777841e-03f, -8.6825294420e-03f,\n        -8.9428499341e-03f, -9.2075373977e-03f, -9.4765927643e-03f, -9.7500113770e-03f,\n        -1.0027793236e-02f, -1.0309931822e-02f, -1.0596422479e-02f, -1.0887259617e-02f,\n        -1.1182436720e-02f, -1.1481943540e-02f, -1.1785773560e-02f, -1.2093913741e-02f,\n        -1.2406354770e-02f, -1.2723082677e-02f, -1.3044086285e-02f, -1.3369349763e-02f,\n        -1.3698859140e-02f, -1.4032597654e-02f, -1.4370549470e-02f, -1.4712693170e-02f,\n        -1.5059012920e-02f, -1.5409486368e-02f, -1.5764094889e-02f, -1.6122814268e-02f,\n        -1.6485624015e-02f, -1.6852496192e-02f, -1.7223412171e-02f, -1.7598342150e-02f\n    }\n};\n\nstatic const float LV2_JX10_EXPECTED_OUT_AFTER_PROGRAM_CHANGE[2][64] = {\n    {\n        -1.8251772970e-02f, -1.8858999014e-02f, -1.9479092211e-02f, -2.0112285390e-02f,\n        -2.0495397970e-02f, -2.0881604403e-02f, -2.1270930767e-02f, -2.1663406864e-02f,\n        -2.2059064358e-02f, -2.2457933053e-02f, -2.2860042751e-02f, -2.3265430704e-02f,\n        -2.3674124852e-02f, -2.4086162448e-02f, -2.4501578882e-02f, -2.4920403957e-02f,\n        -2.5342678651e-02f, -2.5768432766e-02f, -2.6197709143e-02f, -2.6630543172e-02f,\n        -2.7066973969e-02f, -2.7507038787e-02f, -2.7950776741e-02f, -2.8398228809e-02f,\n        -2.8849432245e-02f, -2.9304428026e-02f, -2.9763258994e-02f, -3.0225966126e-02f,\n        -3.0692586675e-02f, -3.1163167208e-02f, -3.1637746841e-02f, -3.2116372138e-02f,\n        -3.2599080354e-02f, -3.3085912466e-02f, -3.3576924354e-02f, -3.4072149545e-02f,\n        -3.4571636468e-02f, -3.5082843155e-02f, -3.5598631948e-02f, -3.6119010299e-02f,\n        -3.6644104868e-02f, -3.7173725665e-02f, -3.7703011185e-02f, -3.8227867335e-02f,\n        -3.8748357445e-02f, -3.9264310151e-02f, -3.9775639772e-02f, -4.0282223374e-02f,\n        -4.0783967823e-02f, -4.1280753911e-02f, -4.1772484779e-02f, -4.2259056121e-02f,\n        -4.2740367353e-02f, -4.3216321617e-02f, -4.3686818331e-02f, -4.4151764363e-02f,\n        -4.4611062855e-02f, -4.5064624399e-02f, -4.5512352139e-02f, -4.5954164118e-02f,\n        -4.6389967203e-02f, -4.6819675714e-02f, -4.7243207693e-02f, -4.7660473734e-02f\n    },\n    {\n        -1.8251772970e-02f, -1.8858999014e-02f, -1.9479092211e-02f, -2.0112285390e-02f,\n        -2.0495397970e-02f, -2.0881604403e-02f, -2.1270930767e-02f, -2.1663406864e-02f,\n        -2.2059064358e-02f, -2.2457933053e-02f, -2.2860042751e-02f, -2.3265430704e-02f,\n        -2.3674124852e-02f, -2.4086162448e-02f, -2.4501578882e-02f, -2.4920403957e-02f,\n        -2.5342678651e-02f, -2.5768432766e-02f, -2.6197709143e-02f, -2.6630543172e-02f,\n        -2.7066973969e-02f, -2.7507038787e-02f, -2.7950776741e-02f, -2.8398228809e-02f,\n        -2.8849432245e-02f, -2.9304428026e-02f, -2.9763258994e-02f, -3.0225966126e-02f,\n        -3.0692586675e-02f, -3.1163167208e-02f, -3.1637746841e-02f, -3.2116372138e-02f,\n        -3.2599080354e-02f, -3.3085912466e-02f, -3.3576924354e-02f, -3.4072149545e-02f,\n        -3.4571636468e-02f, -3.5082843155e-02f, -3.5598631948e-02f, -3.6119010299e-02f,\n        -3.6644104868e-02f, -3.7173725665e-02f, -3.7703011185e-02f, -3.8227867335e-02f,\n        -3.8748357445e-02f, -3.9264310151e-02f, -3.9775639772e-02f, -4.0282223374e-02f,\n        -4.0783967823e-02f, -4.1280753911e-02f, -4.1772484779e-02f, -4.2259056121e-02f,\n        -4.2740367353e-02f, -4.3216321617e-02f, -4.3686818331e-02f, -4.4151764363e-02f,\n        -4.4611062855e-02f, -4.5064624399e-02f, -4.5512352139e-02f, -4.5954164118e-02f,\n        -4.6389967203e-02f, -4.6819675714e-02f, -4.7243207693e-02f, -4.7660473734e-02f\n    }\n};\n\n/*\n * Depends on the MDA JX10 Synth plugin, as ported by drobilla (there are more ports).\n * Since this is relatively heavy to load, several tests are done in one method.\n * 1. Basic program management calls\n * 2. Audio check after note on\n * 3. Audio check after note off\n * 4. Different audio after program change message.\n *\n * If the plugin is not found, the test just returns after printing a message to the console.\n */\nTEST_F(TestLv2Wrapper, TestSynth)\n{\n    SetUp(\"http://drobilla.net/plugins/mda/JX10\");\n\n    ASSERT_TRUE(_module_under_test != nullptr)  << \"'http://drobilla.net/plugins/mda/JX10' plugin not installed - please install it to ensure full suite of unit tests has run.\";\n\n    ChunkSampleBuffer in_buffer(2);\n    ChunkSampleBuffer out_buffer(2);\n\n    ASSERT_TRUE(_module_under_test->supports_programs());\n    ASSERT_EQ(52, _module_under_test->program_count());\n    ASSERT_EQ(0, _module_under_test->current_program());\n\n    ASSERT_EQ(\"http://drobilla.net/plugins/mda/presets#JX10-303-saw-bass\", _module_under_test->current_program_name());\n    auto[status, program_name] = _module_under_test->program_name(2);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    ASSERT_EQ(\"http://drobilla.net/plugins/mda/presets#JX10-5th-sweep-pad\", program_name);\n\n    // Access with an invalid program number\n    std::tie(status, program_name) = _module_under_test->program_name(2000);\n    ASSERT_NE(ProcessorReturnCode::OK, status);\n\n    // Get all programs, all programs are named \"Basic\" in VstXSynth\n    auto[res, programs] = _module_under_test->all_program_names();\n    ASSERT_EQ(ProcessorReturnCode::OK, res);\n    ASSERT_EQ(\"http://drobilla.net/plugins/mda/presets#JX10-fretless-bass\", programs[15]);\n    ASSERT_EQ(52u, programs.size());\n\n    _module_under_test->process_event(RtEvent::make_note_on_event(0, 0, 0, 60, 1.0f));\n    _module_under_test->process_audio(in_buffer, out_buffer);\n\n    test_utils::compare_buffers(LV2_JX10_EXPECTED_OUT_NOTE_ON, out_buffer, 2, 0.0001f);\n\n    _module_under_test->process_event(RtEvent::make_note_off_event(0, 0, 0, 60, 1.0f));\n    _module_under_test->process_audio(in_buffer, out_buffer);\n\n    if(AUDIO_CHUNK_SIZE == 64)\n    {\n        // Buffer comparisons after NOTE_OFF events in LV2 tests require buffer size == 64\n        test_utils::compare_buffers(LV2_JX10_EXPECTED_OUT_NOTE_OFF, out_buffer, 2, 0.0001f);\n    }\n\n    // Setting program once first without checking audio output,\n    // to ensure a sequence of changes goes through, not just the first one.\n    _module_under_test->_pause_audio_processing();\n    _module_under_test->set_program(5);\n    _module_under_test->_resume_audio_processing();\n\n    // A compromise, for the unit tests to be able to run.\n    // It simulates the series of events in the live multithreaded program.\n    _module_under_test->_pause_audio_processing();\n\n    _module_under_test->set_program(1);\n\n    // A compromise, for the unit tests to be able to run.\n    // It simulates the series of events in the live multithreaded program.\n    _module_under_test->_resume_audio_processing();\n\n    _module_under_test->process_event(RtEvent::make_note_on_event(0, 0, 0, 60, 1.0f));\n    _module_under_test->process_audio(in_buffer, out_buffer);\n\n    if(AUDIO_CHUNK_SIZE == 64)\n    {\n        test_utils::compare_buffers(LV2_JX10_EXPECTED_OUT_AFTER_PROGRAM_CHANGE, out_buffer, 2,0.0001f);\n    }\n\n    _module_under_test->process_event(RtEvent::make_note_off_event(0, 0, 0, 60, 1.0f));\n    _module_under_test->process_audio(in_buffer, out_buffer);\n}\n\nTEST_F(TestLv2Wrapper, TestStateHandling)\n{\n    auto ret = SetUp(\"http://lv2plug.in/plugins/eg-amp\");\n    ASSERT_EQ(ProcessorReturnCode::OK, ret);\n\n    auto desc = _module_under_test->parameter_from_name(\"Gain\");\n    ASSERT_TRUE(desc);\n\n    ProcessorState state;\n    state.set_bypass(true);\n    state.set_program(2);\n    state.add_parameter_change(desc->id(), 0.25);\n\n    auto status = _module_under_test->set_state(&state, false);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    // Check that new values are set and update notification is queued\n    EXPECT_FLOAT_EQ(0.25f, _module_under_test->parameter_value(desc->id()).second);\n    EXPECT_TRUE(_module_under_test->bypassed());\n    auto event = _host_control._dummy_dispatcher.retrieve_event();\n    ASSERT_TRUE(event->is_engine_notification());\n\n    // Test with realtime set to true\n    state.set_bypass(false);\n    state.set_program(1);\n    state.add_parameter_change(desc->id(), 0.50);\n\n    status = _module_under_test->set_state(&state, true);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    event = _host_control._dummy_dispatcher.retrieve_event();\n    ASSERT_TRUE(event.get());\n    _module_under_test->process_event(event->to_rt_event(0));\n\n    // Check that new values are set\n    EXPECT_FLOAT_EQ(0.5f, _module_under_test->parameter_value(desc->id()).second);\n    EXPECT_FALSE(_module_under_test->bypassed());\n}\n\nTEST_F(TestLv2Wrapper, TestBinaryStateSaving)\n{\n    ChunkSampleBuffer buffer(2);\n\n    auto ret = SetUp(\"http://lv2plug.in/plugins/eg-amp\");\n    ASSERT_EQ(ProcessorReturnCode::OK, ret);\n\n    // Ugly hack to simulate audio not running.\n    _module_under_test->_model->set_play_state(PlayState::PAUSED);\n\n    auto desc = _module_under_test->parameter_from_name(\"Gain\");\n    ASSERT_TRUE(desc);\n    float prev_value = _module_under_test->parameter_value(desc->id()).second;\n\n    ProcessorState state = _module_under_test->save_state();\n    ASSERT_TRUE(state.has_binary_data());\n\n    // Set a parameter value, the re-apply the state\n    auto rt_event = RtEvent::make_parameter_change_event(_module_under_test->id(), 0, desc->id(), 0.1234f);\n    _module_under_test->process_event(rt_event);\n    _module_under_test->process_audio(buffer, buffer);\n\n    EXPECT_NE(prev_value, _module_under_test->parameter_value(desc->id()).second);\n\n    auto status = _module_under_test->set_state(&state, false);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    // Check the value has reverted to the previous value\n    EXPECT_FLOAT_EQ(prev_value, _module_under_test->parameter_value(desc->id()).second);\n}\n\nTEST_F(TestLv2Wrapper, TestLv2Logging)\n{\n    auto ret = SetUp(\"http://lv2plug.in/plugins/eg-amp\");\n    ASSERT_EQ(ProcessorReturnCode::OK, ret);\n\n    log_buffer.fill('\\0');\n    auto model= _module_under_test->_model.get();\n\n    // Note that LV2Log uses old style printf-formatting\n    lv2_printf(static_cast<void*>(model), model->urids().log_Error, \"logged from %s%i\", \"lv\", 2);\n    EXPECT_STREQ(\"LV2 Error: logged from lv2\", log_buffer.data());\n}\n\n#endif //SUSHI_BUILD_WITH_LV2_MDA_TESTS\n"
  },
  {
    "path": "test/unittests/library/midi_decoder_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"library/midi_decoder.cpp\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::midi;\n\nconst MidiDataByte TEST_NOTE_OFF_MSG  = {0x81, 60, 45, 0};\nconst MidiDataByte TEST_NOTE_ON_MSG   = {0x92, 62, 55, 0};\nconst MidiDataByte TEST_POLY_PRES_MSG = {0xA3, 70, 65, 0};\nconst MidiDataByte TEST_CTRL_CH_MSG   = {0xB4, 67, 75, 0};\nconst MidiDataByte TEST_PROG_CH_MSG   = {0xC5, 18, 0, 0};\nconst MidiDataByte TEST_CHAN_PRES_MSG = {0xD6, 16, 0, 0};\nconst MidiDataByte TEST_PITCH_B_MSG   = {0xE7, 8, 1, 0};\nconst MidiDataByte TEST_TIME_CODE_MSG = {0xF1, 0x35, 0, 0};\nconst MidiDataByte TEST_SONG_POS_MSG  = {0xF2, 0x05, 0x02, 0};\nconst MidiDataByte TEST_SONG_SEL_MSG  = {0xF3, 35, 0, 0};\nconst MidiDataByte TEST_CLOCK_MSG     = {0xF8, 0, 0, 0};\nconst MidiDataByte TEST_START_MSG     = {0xFA, 0, 0, 0};\nconst MidiDataByte TEST_CONTINUE_MSG  = {0xFB, 0, 0, 0};\nconst MidiDataByte TEST_STOP_MSG      = {0xFC, 0, 0, 0};\nconst MidiDataByte TEST_ACTIVE_SNS_MSG= {0xFE, 0, 0, 0};\nconst MidiDataByte TEST_RESET_MSG     = {0xFF, 0, 0, 0};\nconst MidiDataByte TEST_UNKNOWN_MSG   = {0, 0, 0, 0};\n\nTEST (MidiDecoderTest, TestDecodeMessageType) {\n    EXPECT_EQ(MessageType::NOTE_OFF, decode_message_type(TEST_NOTE_OFF_MSG));\n    EXPECT_EQ(MessageType::NOTE_ON, decode_message_type(TEST_NOTE_ON_MSG));\n    EXPECT_EQ(MessageType::POLY_KEY_PRESSURE, decode_message_type(TEST_POLY_PRES_MSG));\n    EXPECT_EQ(MessageType::CONTROL_CHANGE, decode_message_type(TEST_CTRL_CH_MSG));\n    EXPECT_EQ(MessageType::PROGRAM_CHANGE, decode_message_type(TEST_PROG_CH_MSG));\n    EXPECT_EQ(MessageType::CHANNEL_PRESSURE, decode_message_type(TEST_CHAN_PRES_MSG));\n    EXPECT_EQ(MessageType::PITCH_BEND, decode_message_type(TEST_PITCH_B_MSG));\n    EXPECT_EQ(MessageType::TIME_CODE, decode_message_type(TEST_TIME_CODE_MSG));\n    EXPECT_EQ(MessageType::SONG_POSITION, decode_message_type(TEST_SONG_POS_MSG));\n    EXPECT_EQ(MessageType::SONG_SELECT, decode_message_type(TEST_SONG_SEL_MSG));\n\n    // Realtime messages that only consist of 1 byte:\n    EXPECT_EQ(MessageType::TIMING_CLOCK, decode_message_type(TEST_CLOCK_MSG));\n    EXPECT_EQ(MessageType::START, decode_message_type(TEST_START_MSG));\n    EXPECT_EQ(MessageType::CONTINUE, decode_message_type(TEST_CONTINUE_MSG));\n    EXPECT_EQ(MessageType::STOP, decode_message_type(TEST_STOP_MSG));\n    EXPECT_EQ(MessageType::ACTIVE_SENSING, decode_message_type(TEST_ACTIVE_SNS_MSG));\n    EXPECT_EQ(MessageType::RESET, decode_message_type(TEST_RESET_MSG));\n\n    // If message is incomplete it should not be recognized.\n    EXPECT_EQ(MessageType::UNKNOWN, decode_message_type(TEST_UNKNOWN_MSG));\n}\n\nTEST (MidiDecoderTest, TestDecodeChannel)\n{\n    EXPECT_EQ(5, decode_channel({0x35, 0, 0, 0}));\n}\n\nTEST (MidiDecoderTest, TestDecodeNoteOff)\n{\n    NoteOffMessage msg = decode_note_off(TEST_NOTE_OFF_MSG);\n    EXPECT_EQ(1, msg.channel);\n    EXPECT_EQ(60, msg.note);\n    EXPECT_EQ(45, msg.velocity);\n}\n\nTEST (MidiDecoderTest, TestDecodeNoteOn)\n{\n    NoteOnMessage msg = decode_note_on(TEST_NOTE_ON_MSG);\n    EXPECT_EQ(2, msg.channel);\n    EXPECT_EQ(62, msg.note);\n    EXPECT_EQ(55, msg.velocity);\n}\n\nTEST (MidiDecoderTest, TestDecodePolyKeyPressure)\n{\n    PolyKeyPressureMessage msg = decode_poly_key_pressure(TEST_POLY_PRES_MSG);\n    EXPECT_EQ(3, msg.channel);\n    EXPECT_EQ(70, msg.note);\n    EXPECT_EQ(65, msg.pressure);\n}\n\nTEST (ControlChangeMessage, TestDecodeControlChange)\n{\n    ControlChangeMessage msg = decode_control_change(TEST_CTRL_CH_MSG);\n    EXPECT_EQ(4, msg.channel);\n    EXPECT_EQ(67, msg.controller);\n    EXPECT_EQ(75, msg.value);\n}\n\nTEST (ProgramChangeMessage, TestDecodeProgramChange)\n{\n    ProgramChangeMessage msg = decode_program_change(TEST_PROG_CH_MSG);\n    EXPECT_EQ(5, msg.channel);\n    EXPECT_EQ(18, msg.program);\n}\n\nTEST (MidiDecoderTest, TestDecodeChannelPressure)\n{\n    ChannelPressureMessage msg = decode_channel_pressure(TEST_CHAN_PRES_MSG);\n    EXPECT_EQ(6, msg.channel);\n    EXPECT_EQ(16, msg.pressure);\n}\n\nTEST (MidiDecoderTest, TestDecodePitchBend)\n{\n    PitchBendMessage msg = decode_pitch_bend(TEST_PITCH_B_MSG);\n    EXPECT_EQ(7, msg.channel);\n    EXPECT_EQ(136, msg.value);\n}\n\nTEST (MidiDecoderTest, TestDecodeTimeCode)\n{\n    TimeCodeMessage msg = decode_time_code(TEST_TIME_CODE_MSG);\n    EXPECT_EQ(3, msg.message_type);\n    EXPECT_EQ(5, msg.value);\n}\n\nTEST (MidiDecoderTest, TestDecodeSongPosition)\n{\n    SongPositionMessage msg = decode_song_position(TEST_SONG_POS_MSG);\n    EXPECT_EQ(261, msg.beats);\n}\n\nTEST (MidiDecoderTest, TestDecodeSongSelect)\n{\n    SongSelectMessage msg = decode_song_select(TEST_SONG_SEL_MSG);\n    EXPECT_EQ(35, msg.index);\n}\n"
  },
  {
    "path": "test/unittests/library/midi_encoder_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"library/midi_encoder.cpp\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::midi;\n\nTEST(TestMidiEncoder, EncodeNoteOn)\n{\n    auto midi_msg = encode_note_on(1, 48, 1.0f);\n    EXPECT_EQ(0x91, midi_msg[0]);\n    EXPECT_EQ(48u, midi_msg[1]);\n    EXPECT_EQ(127u, midi_msg[2]);\n    EXPECT_EQ(0u, midi_msg[3]);\n}\n\nTEST(TestMidiEncoder, EncodeNoteOff)\n{\n    auto midi_msg = encode_note_off(2, 50, 1.0f);\n    EXPECT_EQ(0x82, midi_msg[0]);\n    EXPECT_EQ(50u, midi_msg[1]);\n    EXPECT_EQ(127u, midi_msg[2]);\n    EXPECT_EQ(0u, midi_msg[3]);\n}\n\nTEST(TestMidiEncoder, EncodePolyKeyPressure)\n{\n    auto midi_msg = encode_poly_key_pressure(3, 52, 1.0f);\n    EXPECT_EQ(0xA3, midi_msg[0]);\n    EXPECT_EQ(52u, midi_msg[1]);\n    EXPECT_EQ(127u, midi_msg[2]);\n    EXPECT_EQ(0u, midi_msg[3]);\n}\n\nTEST(TestMidiEncoder, EncodeControlChange)\n{\n    auto midi_msg = encode_control_change(4, 12, 1.0f);\n    EXPECT_EQ(0xB4, midi_msg[0]);\n    EXPECT_EQ(12u, midi_msg[1]);\n    EXPECT_EQ(127u, midi_msg[2]);\n    EXPECT_EQ(0u, midi_msg[3]);\n}\n\nTEST(TestMidiEncoder, EncodeChannelPressure)\n{\n    auto midi_msg = encode_channel_pressure(5, 1.0f);\n    EXPECT_EQ(0xD5, midi_msg[0]);\n    EXPECT_EQ(127u, midi_msg[1]);\n    EXPECT_EQ(0u, midi_msg[2]);\n    EXPECT_EQ(0u, midi_msg[3]);\n}\n\nTEST(TestMidiEncoder, EncodePitchBend)\n{\n    auto midi_msg = encode_pitch_bend(6, 1.0f);\n    int pb = midi_msg[1] + (midi_msg[2] << 7);\n    EXPECT_EQ(0xE6, midi_msg[0]);\n    EXPECT_EQ(MAX_PITCH_BEND, pb);\n\n    midi_msg = encode_pitch_bend(7, 0.0f);\n    pb = midi_msg[1] + (midi_msg[2] << 7) - PITCH_BEND_MIDDLE;\n    EXPECT_EQ(0xE7, midi_msg[0]);\n    EXPECT_EQ(0, pb);\n}\n\nTEST(TestMidiEncoder, EncodeProgramChange)\n{\n    auto midi_msg = encode_program_change(7, 53);\n    EXPECT_EQ(0xC7, midi_msg[0]);\n    EXPECT_EQ(53u, midi_msg[1]);\n    EXPECT_EQ(0u, midi_msg[2]);\n    EXPECT_EQ(0u, midi_msg[3]);\n}\n\nTEST(TestMidiEncoder, EncodeStartMessage)\n{\n    auto midi_msg = encode_start_message();\n    EXPECT_EQ(0xFA, midi_msg[0]);\n    EXPECT_EQ(0u, midi_msg[1]);\n    EXPECT_EQ(0u, midi_msg[2]);\n    EXPECT_EQ(0u, midi_msg[3]);\n}\n\nTEST(TestMidiEncoder, EncodeStopMessage)\n{\n    auto midi_msg = encode_stop_message();\n    EXPECT_EQ(0xFC, midi_msg[0]);\n    EXPECT_EQ(0u, midi_msg[1]);\n    EXPECT_EQ(0u, midi_msg[2]);\n    EXPECT_EQ(0u, midi_msg[3]);\n}\n\nTEST(TestMidiEncoder, EncodeContinueMessage)\n{\n    auto midi_msg = encode_continue_message();\n    EXPECT_EQ(0xFB, midi_msg[0]);\n    EXPECT_EQ(0u, midi_msg[1]);\n    EXPECT_EQ(0u, midi_msg[2]);\n    EXPECT_EQ(0u, midi_msg[3]);\n}\n\nTEST(TestMidiEncoder, EncodeTimingClock)\n{\n    auto midi_msg = encode_timing_clock();\n    EXPECT_EQ(0xF8, midi_msg[0]);\n    EXPECT_EQ(0u, midi_msg[1]);\n    EXPECT_EQ(0u, midi_msg[2]);\n    EXPECT_EQ(0u, midi_msg[3]);\n}\n\nTEST(TestMidiEncoder, EncodeActiveSensing)\n{\n    auto midi_msg = encode_active_sensing();\n    EXPECT_EQ(0xFE, midi_msg[0]);\n    EXPECT_EQ(0u, midi_msg[1]);\n    EXPECT_EQ(0u, midi_msg[2]);\n    EXPECT_EQ(0u, midi_msg[3]);\n}\n\nTEST(TestMidiEncoder, EncodeResetMessage)\n{\n    auto midi_msg = encode_reset_message();\n    EXPECT_EQ(0xFF, midi_msg[0]);\n    EXPECT_EQ(0u, midi_msg[1]);\n    EXPECT_EQ(0u, midi_msg[2]);\n    EXPECT_EQ(0u, midi_msg[3]);\n}"
  },
  {
    "path": "test/unittests/library/parameter_dump_test.cpp",
    "content": "#include <fstream>\n\n#include \"gtest/gtest.h\"\n#include \"library/parameter_dump.cpp\"\n#include \"test_utils/control_mockup.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\nELK_PUSH_WARNING\nELK_DISABLE_COMPARISON_CALLS_NAME_RECURSIVELY\n\nTEST(TestParameterDump, TestParameterDocumentGeneration)\n{\n    const std::string expected_result_str = \"{\\n    \\\"plugins\\\": [\\n        {\\n            \\\"name\\\": \\\"proc 1\\\",\\n            \\\"label\\\": \\\"proc 1\\\",\\n            \\\"processor_id\\\": 0,\\n            \\\"parent_track_id\\\": 0,\\n            \\\"parameters\\\": [\\n                {\\n                    \\\"name\\\": \\\"param 1\\\",\\n                    \\\"label\\\": \\\"param 1\\\",\\n                    \\\"osc_path\\\": \\\"/parameter/proc_1/param_1\\\",\\n                    \\\"id\\\": 0\\n                },\\n                {\\n                    \\\"name\\\": \\\"param 2\\\",\\n                    \\\"label\\\": \\\"param 2\\\",\\n                    \\\"osc_path\\\": \\\"/parameter/proc_1/param_2\\\",\\n                    \\\"id\\\": 1\\n                },\\n                {\\n                    \\\"name\\\": \\\"param 3\\\",\\n                    \\\"label\\\": \\\"param 3\\\",\\n                    \\\"osc_path\\\": \\\"/parameter/proc_1/param_3\\\",\\n                    \\\"id\\\": 2\\n                }\\n            ]\\n        },\\n        {\\n            \\\"name\\\": \\\"proc 2\\\",\\n            \\\"label\\\": \\\"proc 2\\\",\\n            \\\"processor_id\\\": 1,\\n            \\\"parent_track_id\\\": 0,\\n            \\\"parameters\\\": [\\n                {\\n                    \\\"name\\\": \\\"param 1\\\",\\n                    \\\"label\\\": \\\"param 1\\\",\\n                    \\\"osc_path\\\": \\\"/parameter/proc_2/param_1\\\",\\n                    \\\"id\\\": 0\\n                },\\n                {\\n                    \\\"name\\\": \\\"param 2\\\",\\n                    \\\"label\\\": \\\"param 2\\\",\\n                    \\\"osc_path\\\": \\\"/parameter/proc_2/param_2\\\",\\n                    \\\"id\\\": 1\\n                },\\n                {\\n                    \\\"name\\\": \\\"param 3\\\",\\n                    \\\"label\\\": \\\"param 3\\\",\\n                    \\\"osc_path\\\": \\\"/parameter/proc_2/param_3\\\",\\n                    \\\"id\\\": 2\\n                }\\n            ]\\n        },\\n        {\\n            \\\"name\\\": \\\"proc 1\\\",\\n            \\\"label\\\": \\\"proc 1\\\",\\n            \\\"processor_id\\\": 0,\\n            \\\"parent_track_id\\\": 1,\\n            \\\"parameters\\\": [\\n                {\\n                    \\\"name\\\": \\\"param 1\\\",\\n                    \\\"label\\\": \\\"param 1\\\",\\n                    \\\"osc_path\\\": \\\"/parameter/proc_1/param_1\\\",\\n                    \\\"id\\\": 0\\n                },\\n                {\\n                    \\\"name\\\": \\\"param 2\\\",\\n                    \\\"label\\\": \\\"param 2\\\",\\n                    \\\"osc_path\\\": \\\"/parameter/proc_1/param_2\\\",\\n                    \\\"id\\\": 1\\n                },\\n                {\\n                    \\\"name\\\": \\\"param 3\\\",\\n                    \\\"label\\\": \\\"param 3\\\",\\n                    \\\"osc_path\\\": \\\"/parameter/proc_1/param_3\\\",\\n                    \\\"id\\\": 2\\n                }\\n            ]\\n        },\\n        {\\n            \\\"name\\\": \\\"proc 2\\\",\\n            \\\"label\\\": \\\"proc 2\\\",\\n            \\\"processor_id\\\": 1,\\n            \\\"parent_track_id\\\": 1,\\n            \\\"parameters\\\": [\\n                {\\n                    \\\"name\\\": \\\"param 1\\\",\\n                    \\\"label\\\": \\\"param 1\\\",\\n                    \\\"osc_path\\\": \\\"/parameter/proc_2/param_1\\\",\\n                    \\\"id\\\": 0\\n                },\\n                {\\n                    \\\"name\\\": \\\"param 2\\\",\\n                    \\\"label\\\": \\\"param 2\\\",\\n                    \\\"osc_path\\\": \\\"/parameter/proc_2/param_2\\\",\\n                    \\\"id\\\": 1\\n                },\\n                {\\n                    \\\"name\\\": \\\"param 3\\\",\\n                    \\\"label\\\": \\\"param 3\\\",\\n                    \\\"osc_path\\\": \\\"/parameter/proc_2/param_3\\\",\\n                    \\\"id\\\": 2\\n                }\\n            ]\\n        }\\n    ]\\n}\";\n    rapidjson::Document expected_result;\n    expected_result.Parse(expected_result_str.c_str());\n    sushi::control::ControlMockup controller;\n\n    rapidjson::Document result = sushi::generate_processor_parameter_document(&controller);\n\n    ASSERT_TRUE(expected_result == result);\n}\n\nELK_POP_WARNING"
  },
  {
    "path": "test/unittests/library/performance_timer_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"library/performance_timer.cpp\"\n\nnamespace sushi::internal::performance\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(PerformanceTimer& f) : _friend(f) {}\n\n    // Not const: it's modified in the test\n    std::atomic_bool& enabled()\n    {\n        return _friend._enabled;\n    }\n\n    void update_timings()\n    {\n        _friend._update_timings();\n    }\n\nprivate:\n    PerformanceTimer& _friend;\n};\n\n}\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::performance;\n\nconstexpr auto TEST_PERIOD = std::chrono::microseconds(100);\n\nperformance::TimePoint virtual_wait(const performance::TimePoint& tp, int n)\n{\n    /* \"Wait\" by rewinding the timestamp, makes the test robust against\n     * threading and scheduling issues */\n    return tp - n * (TEST_PERIOD / 10);\n}\n\nvoid run_test_scenario(PerformanceTimer& timer)\n{\n    auto start = timer.start_timer();\n    start = virtual_wait(start, 1);\n    timer.stop_timer(start, 1);\n\n    start = timer.start_timer();\n    start = virtual_wait(start, 1);\n    timer.stop_timer(start, 1);\n\n    start = timer.start_timer();\n    start = virtual_wait(start, 5);\n    timer.stop_timer(start, 2);\n\n    start = timer.start_timer();\n    start = virtual_wait(start, 3);\n    timer.stop_timer(start, 2);\n}\n\nclass TestPerformanceTimer : public ::testing::Test\n{\nprotected:\n    TestPerformanceTimer() = default;\n\n    void SetUp() override\n    {\n        _module_under_test.set_timing_period(TEST_PERIOD);\n        /* Hack to store records while not using the worker thread */\n        _accessor.enabled() = true;\n    }\n\n    PerformanceTimer _module_under_test;\n\n    Accessor _accessor {_module_under_test};\n};\n\n\nTEST_F(TestPerformanceTimer, TestOperation)\n{\n    run_test_scenario(_module_under_test);\n    _accessor.update_timings();\n\n    auto timings_1 = _module_under_test.timings_for_node(1);\n    ASSERT_TRUE(timings_1.has_value());\n    auto timings_2 = _module_under_test.timings_for_node(2);\n    ASSERT_TRUE(timings_1.has_value());\n    auto timings_467 = _module_under_test.timings_for_node(467);\n    ASSERT_FALSE(timings_467.has_value());\n\n    auto t1 = timings_1.value();\n    auto t2 = timings_2.value();\n\n    ASSERT_TRUE(t1.min_case > 0);\n    ASSERT_TRUE(t1.avg_case > 0);\n    ASSERT_GE(t1.max_case, t1.min_case);\n    ASSERT_TRUE(t2.min_case > 0);\n    ASSERT_TRUE(t2.avg_case > 0);\n    ASSERT_GE(t2.max_case, t2.min_case);\n\n    ASSERT_GE(t2.max_case, t1.max_case);\n    ASSERT_GE(t2.avg_case, t1.avg_case);\n}\n\nTEST_F(TestPerformanceTimer, TestClearRecords)\n{\n    run_test_scenario(_module_under_test);\n    _accessor.update_timings();\n\n    ASSERT_TRUE(_module_under_test.clear_timings_for_node(2));\n    ASSERT_FALSE(_module_under_test.clear_timings_for_node(467));\n\n    auto timings = _module_under_test.timings_for_node(2);\n    ASSERT_TRUE(timings.has_value());\n    auto t = timings.value();\n\n    ASSERT_FLOAT_EQ(0.0f, t.avg_case);\n    ASSERT_FLOAT_EQ(100.0f, t.min_case);\n    ASSERT_FLOAT_EQ(0.0f, t.max_case);\n\n    _module_under_test.clear_all_timings();\n\n    timings = _module_under_test.timings_for_node(1);\n    ASSERT_TRUE(timings.has_value());\n    t = timings.value();\n\n    ASSERT_FLOAT_EQ(0.0f, t.avg_case);\n    ASSERT_FLOAT_EQ(100.0f, t.min_case);\n    ASSERT_FLOAT_EQ(0.0f, t.max_case);\n}\n"
  },
  {
    "path": "test/unittests/library/plugin_parameters_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"library/plugin_parameters.h\"\n#include \"test_utils/test_utils.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\n\nclass TestdBToLinPreProcessor : public ::testing::Test\n{\nprotected:\n    TestdBToLinPreProcessor() {}\n\n    dBToLinPreProcessor _module_under_test{-24.0f, 24.0f};\n};\n\nTEST_F(TestdBToLinPreProcessor, TestProcessing)\n{\n    EXPECT_NEAR(1.0, _module_under_test.process_to_plugin(_module_under_test.to_domain(0.5f)), test_utils::DECIBEL_ERROR);\n    EXPECT_NEAR(2.0, _module_under_test.process_to_plugin(_module_under_test.to_domain(0.625f)), test_utils::DECIBEL_ERROR);\n    EXPECT_NEAR(0.25, _module_under_test.process_to_plugin(_module_under_test.to_domain(0.25f)), test_utils::DECIBEL_ERROR);\n}\n\nclass TestLinTodBPreProcessor : public ::testing::Test\n{\nprotected:\n    TestLinTodBPreProcessor() {}\n\n    LinTodBPreProcessor _module_under_test{0, 10.0f};\n};\n\nTEST_F(TestLinTodBPreProcessor, TestProcessing)\n{\n    EXPECT_NEAR(0.0f, _module_under_test.process_to_plugin(_module_under_test.to_domain(0.1f)), test_utils::DECIBEL_ERROR);\n    EXPECT_NEAR(6.02f, _module_under_test.process_to_plugin(_module_under_test.to_domain(0.2f)), test_utils::DECIBEL_ERROR);\n    EXPECT_NEAR(-12.04f, _module_under_test.process_to_plugin(_module_under_test.to_domain(0.025f)), test_utils::DECIBEL_ERROR);\n}\n\nclass TestCubicWarpPreProcessor : public ::testing::Test\n{\nprotected:\n    TestCubicWarpPreProcessor() {}\n\n    CubicWarpPreProcessor _module_under_test{20.0f, 20'000.0f};\n};\n\nTEST_F(TestCubicWarpPreProcessor, TestProcessing)\n{\n    EXPECT_NEAR(20.0f, _module_under_test.process_to_plugin(_module_under_test.to_domain(0.0f)), 1.0e-6);\n    EXPECT_NEAR(2'517.5f, _module_under_test.process_to_plugin(_module_under_test.to_domain(0.5f)), 1.0e-6);\n    EXPECT_NEAR(20'000.0f, _module_under_test.process_to_plugin(_module_under_test.to_domain(1.0f)), 1.0e-6);\n}\n\nTEST_F(TestCubicWarpPreProcessor, TestIdentity)\n{\n    EXPECT_NEAR(0.5f, _module_under_test.to_normalized(_module_under_test.process_to_plugin(_module_under_test.to_domain(0.5f))), 1.0e-6);\n}\n\n\n/*\n * Templated testing is difficult since we want to test with different values for each type\n * Therefore we test each type separately.\n */\n\nclass TestParameter : public ::testing::Test\n{\nprotected:\n    TestParameter() {}\n\n    uint8_t* TEST_DATA = new uint8_t[3];\n    BlobData blob{3, TEST_DATA};\n\n    FloatParameterDescriptor _module_under_test_float{\"float_parameter\",\n                                                      \"FloatParameter\",\n                                                      \"fl\",\n                                                      -10.0f,\n                                                      10.0f,\n                                                      Direction::AUTOMATABLE,\n                                                      new ParameterPreProcessor<float>(-10, 10)};\n\n    IntParameterDescriptor _module_under_test_int{\"int_parameter\",\n                                                  \"IntParameter\",\n                                                  \"int\",\n                                                  -10,\n                                                  10,\n                                                  Direction::OUTPUT,\n                                                  new ParameterPreProcessor<int>(-10, 10)};\n\n    BoolParameterDescriptor _module_under_test_bool{\"bool_parameter\",\n                                                    \"BoolParameter\",\n                                                    \"bool\",\n                                                    false,\n                                                    true,\n                                                    Direction::AUTOMATABLE,\n                                                    new ParameterPreProcessor<bool>(0, 1)};\n\n    StringPropertyDescriptor _module_under_test_string{\"string_property\", \"String Property\", \"\", Direction::OUTPUT};\n    DataPropertyDescriptor _module_under_test_data{\"data_property\", \"Data Property\", \"data\"};\n};\n\nTEST_F(TestParameter, TestTypeNameAndLabel)\n{\n    EXPECT_EQ(ParameterType::BOOL, _module_under_test_bool.type());\n    EXPECT_EQ(ParameterType::FLOAT, _module_under_test_float.type());\n    EXPECT_EQ(ParameterType::INT, _module_under_test_int.type());\n    EXPECT_EQ(ParameterType::STRING, _module_under_test_string.type());\n    EXPECT_EQ(ParameterType::DATA, _module_under_test_data.type());\n\n    EXPECT_EQ(\"bool_parameter\", _module_under_test_bool.name());\n    EXPECT_EQ(\"float_parameter\", _module_under_test_float.name());\n    EXPECT_EQ(\"int_parameter\", _module_under_test_int.name());\n    EXPECT_EQ(\"string_property\", _module_under_test_string.name());\n    EXPECT_EQ(\"data_property\", _module_under_test_data.name());\n\n    EXPECT_EQ(\"BoolParameter\", _module_under_test_bool.label());\n    EXPECT_EQ(\"FloatParameter\", _module_under_test_float.label());\n    EXPECT_EQ(\"IntParameter\", _module_under_test_int.label());\n    EXPECT_EQ(\"String Property\", _module_under_test_string.label());\n    EXPECT_EQ(\"Data Property\", _module_under_test_data.label());\n\n    EXPECT_EQ(\"bool\", _module_under_test_bool.unit());\n    EXPECT_EQ(\"fl\", _module_under_test_float.unit());\n    EXPECT_EQ(\"int\", _module_under_test_int.unit());\n    EXPECT_EQ(\"\", _module_under_test_string.unit());\n    EXPECT_EQ(\"data\", _module_under_test_data.unit());\n\n    EXPECT_TRUE(_module_under_test_bool.automatable());\n    EXPECT_TRUE(_module_under_test_float.automatable());\n    EXPECT_FALSE(_module_under_test_int.automatable());\n    EXPECT_FALSE(_module_under_test_string.automatable());\n    EXPECT_FALSE(_module_under_test_data.automatable());\n}\n\nTEST(TestParameterValue, TestSet)\n{\n    dBToLinPreProcessor pre_processor(-6.0f, 6.0f);\n    auto value = ParameterStorage::make_float_parameter_storage(nullptr, 0.0f, &pre_processor);\n    /* Check correct defaults */\n    EXPECT_EQ(ParameterType::FLOAT, value.float_parameter_value()->type());\n    EXPECT_FLOAT_EQ(1.0f, value.float_parameter_value()->processed_value());\n    EXPECT_FLOAT_EQ(0.0f, value.float_parameter_value()->domain_value());\n\n    /* Test set */\n    value.float_parameter_value()->set(pre_processor.to_normalized(6.0f));\n    EXPECT_NEAR(2.0f, value.float_parameter_value()->processed_value(), 0.01f);\n    EXPECT_FLOAT_EQ(6.0f, value.float_parameter_value()->domain_value());\n}\n"
  },
  {
    "path": "test/unittests/library/processor_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"engine/transport.h\"\n\n#include \"test_utils/test_utils.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"library/processor.cpp\"\n\n#include \"test_utils/host_control_mockup.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\n\nnamespace sushi::internal\n{\n\nclass ProcessorAccessor\n{\npublic:\n    explicit ProcessorAccessor(Processor& plugin) : _friend(plugin) {}\n\n    [[nodiscard]] bool register_parameter(ParameterDescriptor* parameter)\n    {\n        return _friend.register_parameter(parameter);\n    }\n\n    [[nodiscard]] std::string make_unique_parameter_name(const std::string& name) const\n    {\n        return _friend._make_unique_parameter_name(name);\n    }\n\n    void bypass_process(const ChunkSampleBuffer& in_buffer, ChunkSampleBuffer& out_buffer)\n    {\n        _friend.bypass_process(in_buffer, out_buffer);\n    }\n\n    [[nodiscard]] bool maybe_output_cv_value(ObjectId parameter_id, float value)\n    {\n        return _friend.maybe_output_cv_value(parameter_id, value);\n    }\n\n    [[nodiscard]] bool maybe_output_gate_event(int channel, int note, bool note_on)\n    {\n        return _friend.maybe_output_gate_event(channel, note, note_on);\n    }\n\nprivate:\n    Processor& _friend;\n};\n\n}\n\nconstexpr float TEST_SAMPLE_RATE = 44100;\n\nconstexpr int TEST_BYPASS_TIME_MS = 13;\n\n/* Implement dummies of virtual methods to we can instantiate a test class */\nclass ProcessorTest : public Processor\n{\npublic:\n    explicit ProcessorTest(HostControl host_control) : Processor(host_control)\n    {\n        _max_input_channels = 2;\n        _max_output_channels = 2;\n    }\n\n    ~ProcessorTest() override = default;\n\n    void process_audio(const ChunkSampleBuffer& /*in_buffer*/,\n                       ChunkSampleBuffer& /*out_buffer*/) override {}\n\n    void process_event(const RtEvent& /*event*/) override {}\n};\n\n\nclass TestProcessor : public ::testing::Test\n{\nprotected:\n    TestProcessor() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<ProcessorTest>(_host_control.make_host_control_mockup());\n\n        _accessor = std::make_unique<sushi::internal::ProcessorAccessor>(*_module_under_test);\n    }\n\n    void TearDown() override\n    {\n    }\n    \n    HostControlMockup _host_control;\n    RtEventFifo<10> _event_queue;\n\n    std::unique_ptr<Processor> _module_under_test;\n\n    std::unique_ptr<sushi::internal::ProcessorAccessor> _accessor;\n};\n\nTEST_F(TestProcessor, TestBasicProperties)\n{\n    /* Set the common properties and verify the changes are applied */\n    _module_under_test->set_name(std::string(\"Processor 1\"));\n    EXPECT_EQ(_module_under_test->name(), \"Processor 1\");\n\n    _module_under_test->set_label(\"processor_1\");\n    EXPECT_EQ(\"processor_1\", _module_under_test->label());\n\n    _module_under_test->set_enabled(true);\n    EXPECT_TRUE(_module_under_test->enabled());\n}\n\nTEST_F(TestProcessor, TestParameterHandling)\n{\n    /* Register a single parameter and verify accessor functions */\n    auto p = new FloatParameterDescriptor(\"param\", \"Float\", \"fl\", 0, 1, Direction::AUTOMATABLE, nullptr);\n    bool success = _accessor->register_parameter(p);\n    ASSERT_TRUE(success);\n\n    auto param = _module_under_test->parameter_from_name(\"not_found\");\n    EXPECT_FALSE(param);\n    param = _module_under_test->parameter_from_name(\"param\");\n    EXPECT_TRUE(param);\n\n    ObjectId id = param->id();\n    param = _module_under_test->parameter_from_id(id);\n    EXPECT_TRUE(param);\n    param = _module_under_test->parameter_from_id(1000);\n    EXPECT_FALSE(param);\n\n    auto param_list = _module_under_test->all_parameters();\n    EXPECT_EQ(1u, param_list.size());\n}\n\nTEST_F(TestProcessor, TestDuplicateParameterNames)\n{\n    bool success = _accessor->register_parameter(new FloatParameterDescriptor(\"param\", \"Float\", \"fl\",\n                                                                              0, 1, Direction::AUTOMATABLE, nullptr));\n    ASSERT_TRUE(success);\n\n    // Test uniqueness by entering an already existing parameter name\n    EXPECT_EQ(\"param_2\", _accessor->make_unique_parameter_name(\"param\"));\n    EXPECT_EQ(\"parameter\", _accessor->make_unique_parameter_name(\"\"));\n}\n\nTEST_F(TestProcessor, TestBypassProcessing)\n{\n    ChunkSampleBuffer buffer(2);\n    ChunkSampleBuffer out_buffer(2);\n    ChunkSampleBuffer mono_buffer(1);\n    test_utils::fill_sample_buffer(buffer, 1.0f);\n    test_utils::fill_sample_buffer(mono_buffer, 2.0f);\n\n    _module_under_test->set_channels(2, 2);\n    // Stereo into stereo\n    _accessor->bypass_process(buffer, out_buffer);\n    test_utils::assert_buffer_value(1.0f, out_buffer);\n\n    // Mono into stereo\n    _module_under_test->set_channels(1, 2);\n    _accessor->bypass_process(mono_buffer, out_buffer);\n    test_utils::assert_buffer_value(2.0f, out_buffer);\n\n    // No input should clear output\n    _module_under_test->set_channels(0, 2);\n    _accessor->bypass_process(buffer, out_buffer);\n    test_utils::assert_buffer_value(0.0f, out_buffer);\n}\n\nTEST_F(TestProcessor, TestCvOutput)\n{\n    auto p = new FloatParameterDescriptor(\"param\", \"Float\", \"\", 0, 1, Direction::AUTOMATABLE, nullptr);\n    bool success = _accessor->register_parameter(p);\n    ASSERT_TRUE(success);\n\n    _module_under_test->set_event_output(&_event_queue);\n    auto param = _module_under_test->parameter_from_name(\"param\");\n    ASSERT_TRUE(param);\n\n    // Output parameter update\n    auto success_cv_out = _accessor->maybe_output_cv_value(param->id(), 0.5f);\n    ASSERT_FALSE(success_cv_out);\n    ASSERT_TRUE(_event_queue.empty());\n\n    // Connect parameter to CV output and send update\n    auto res = _module_under_test->connect_cv_from_parameter(param->id(), 1);\n    ASSERT_EQ(ProcessorReturnCode::OK, res);\n    success_cv_out = _accessor->maybe_output_cv_value(param->id(), 0.25f);\n    ASSERT_TRUE(success_cv_out);\n    ASSERT_FALSE(_event_queue.empty());\n    auto cv_event = _event_queue.pop();\n    EXPECT_EQ(RtEventType::CV_EVENT, cv_event.type());\n    EXPECT_EQ(1, cv_event.cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.25f, cv_event.cv_event()->value());\n}\n\nTEST_F(TestProcessor, TestGateOutput)\n{\n    _module_under_test->set_event_output(&_event_queue);\n\n    // Output gate update with no connections\n    auto success = _accessor->maybe_output_gate_event(5, 10, true);\n    ASSERT_FALSE(success);\n\n    // Connect to gate output and send update with another note/channel combo\n    auto res = _module_under_test->connect_gate_from_processor(1, 5, 10);\n    ASSERT_EQ(ProcessorReturnCode::OK, res);\n    success = _accessor->maybe_output_gate_event(4, 9, true);\n    ASSERT_FALSE(success);\n\n    // Output gate event\n    success = _accessor->maybe_output_gate_event(5, 10, true);\n    ASSERT_TRUE(success);\n    ASSERT_FALSE(_event_queue.empty());\n    auto event = _event_queue.pop();\n    EXPECT_EQ(RtEventType::GATE_EVENT, event.type());\n    EXPECT_EQ(1, event.gate_event()->gate_no());\n    EXPECT_TRUE(event.gate_event()->value());\n}\n\nclass TestBypassManager : public ::testing::Test\n{\nprotected:\n    TestBypassManager() = default;\n\n    BypassManager _module_under_test{false, std::chrono::milliseconds(TEST_BYPASS_TIME_MS)};\n};\n\nTEST_F(TestBypassManager, TestOperation)\n{\n    EXPECT_FALSE(_module_under_test.bypassed());\n    EXPECT_TRUE(_module_under_test.should_process());\n    EXPECT_FALSE(_module_under_test.should_ramp());\n\n    // Set the same condition, nothing should change\n    _module_under_test.set_bypass(false, TEST_SAMPLE_RATE);\n    EXPECT_FALSE(_module_under_test.bypassed());\n    EXPECT_TRUE(_module_under_test.should_process());\n    EXPECT_FALSE(_module_under_test.should_ramp());\n\n    // Set bypass on\n    _module_under_test.set_bypass(true, TEST_SAMPLE_RATE);\n    EXPECT_TRUE(_module_under_test.bypassed());\n    EXPECT_TRUE(_module_under_test.should_process());\n    EXPECT_TRUE(_module_under_test.should_ramp());\n}\n\nTEST_F(TestBypassManager, TestSetBypassRampTime)\n{\n    int expected_chunks = static_cast<int>((TEST_SAMPLE_RATE * TEST_BYPASS_TIME_MS * 0.001) / AUDIO_CHUNK_SIZE);\n\n    // With some sample rate and buffer size combinations this is false.\n    if (expected_chunks <= 0)\n    {\n        // But also in those cases, we want to test with at least one chunk.\n        expected_chunks = 1;\n    }\n\n    // ... Because chunks_to_rap returns a minimum of 1.\n    int to_ramp = _module_under_test.chunks_to_ramp(TEST_SAMPLE_RATE);\n\n    EXPECT_EQ(expected_chunks, to_ramp);\n}\n\nTEST_F(TestBypassManager, TestRamping)\n{\n    int chunks_in_ramp = static_cast<int>((TEST_SAMPLE_RATE * TEST_BYPASS_TIME_MS * 0.001f) / AUDIO_CHUNK_SIZE);\n\n    // With some sample rate and buffer size combinations this is false.\n    if (chunks_in_ramp <= 0)\n    {\n        // But also in those cases, we want to test with at least one chunk.\n        chunks_in_ramp = 1;\n    }\n\n    ChunkSampleBuffer buffer(2);\n    _module_under_test.set_bypass(true, TEST_SAMPLE_RATE);\n    EXPECT_TRUE(_module_under_test.should_ramp());\n\n    for (int i = 0; i < chunks_in_ramp; ++i)\n    {\n        test_utils::fill_sample_buffer(buffer, 1.0f);\n        _module_under_test.ramp_output(buffer);\n    }\n\n    // We should now have ramped down to 0\n    EXPECT_NEAR(0.0f, buffer.channel(0)[AUDIO_CHUNK_SIZE - 1], 1.0e-7);\n    EXPECT_NEAR(0.0f, buffer.channel(1)[AUDIO_CHUNK_SIZE - 1], 1.0e-7);\n    EXPECT_FLOAT_EQ(1.0f / chunks_in_ramp, buffer.channel(0)[0]);\n    EXPECT_FLOAT_EQ(1.0f / chunks_in_ramp, buffer.channel(1)[0]);\n\n    EXPECT_FALSE(_module_under_test.should_ramp());\n\n    // Turn it on again (bypass = false)\n    _module_under_test.set_bypass(false, TEST_SAMPLE_RATE);\n    EXPECT_TRUE(_module_under_test.should_ramp());\n\n    for (int i = 0; i < chunks_in_ramp; ++i)\n    {\n        test_utils::fill_sample_buffer(buffer, 1.0f);\n        _module_under_test.ramp_output(buffer);\n    }\n\n    // We should have ramped up to full volume again\n    EXPECT_FLOAT_EQ(1.0f, buffer.channel(0)[AUDIO_CHUNK_SIZE - 1]);\n    EXPECT_FLOAT_EQ(1.0f, buffer.channel(1)[AUDIO_CHUNK_SIZE - 1]);\n    EXPECT_FLOAT_EQ((chunks_in_ramp - 1.0f) / chunks_in_ramp, buffer.channel(0)[0]);\n    EXPECT_FLOAT_EQ((chunks_in_ramp - 1.0f) / chunks_in_ramp, buffer.channel(1)[0]);\n\n    EXPECT_FALSE(_module_under_test.should_ramp());\n}\n\nTEST_F(TestBypassManager, TestCrossfade)\n{\n    int chunks_in_ramp = static_cast<int>((TEST_SAMPLE_RATE * TEST_BYPASS_TIME_MS * 0.001f) / AUDIO_CHUNK_SIZE);\n    ChunkSampleBuffer buffer(2);\n    ChunkSampleBuffer bypass_buffer(2);\n    test_utils::fill_sample_buffer(buffer, 2.0f);\n    test_utils::fill_sample_buffer(bypass_buffer, 1);\n    _module_under_test.set_bypass(true, TEST_SAMPLE_RATE);\n    EXPECT_TRUE(_module_under_test.should_ramp());\n\n    _module_under_test.crossfade_output(bypass_buffer, buffer, 2, 2);\n\n    EXPECT_LE(buffer.channel(1)[AUDIO_CHUNK_SIZE - 1], 2.0f);\n    EXPECT_GE(buffer.channel(1)[AUDIO_CHUNK_SIZE - 1], 1.0f);\n\n    for (int i = 0; i < chunks_in_ramp - 1; ++i)\n    {\n        test_utils::fill_sample_buffer(buffer, 2.0f);\n        _module_under_test.crossfade_output(bypass_buffer, buffer, 2, 2);\n    }\n\n    // We should now have ramped down to 1 (value of bypass buffer)\n    EXPECT_FLOAT_EQ(1.0f, buffer.channel(0)[AUDIO_CHUNK_SIZE - 1]);\n    EXPECT_FLOAT_EQ(1.0f, buffer.channel(1)[AUDIO_CHUNK_SIZE - 1]);\n}\n"
  },
  {
    "path": "test/unittests/library/rt_event_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"library/rt_event.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\n\nTEST (TestRealtimeEvents, TestFactoryFunction)\n{\n    auto event = RtEvent::make_note_on_event(123, 1, 0, 46, 0.5f);\n    EXPECT_EQ(RtEventType::NOTE_ON, event.type());\n    auto note_on_event = event.keyboard_event();\n    EXPECT_EQ(ObjectId(123), note_on_event->processor_id());\n    EXPECT_EQ(1, note_on_event->sample_offset());\n    EXPECT_EQ(0, note_on_event->channel());\n    EXPECT_EQ(46, note_on_event->note());\n    EXPECT_FLOAT_EQ(0.5f, note_on_event->velocity());\n\n    event = RtEvent::make_note_off_event(122, 2, 1, 47, 0.5f);\n    EXPECT_EQ(RtEventType::NOTE_OFF, event.type());\n    auto note_off_event = event.keyboard_event();\n    EXPECT_EQ(ObjectId(122), note_off_event->processor_id());\n    EXPECT_EQ(2, note_off_event->sample_offset());\n    EXPECT_EQ(1, note_on_event->channel());\n    EXPECT_EQ(47, note_off_event->note());\n    EXPECT_FLOAT_EQ(0.5f, note_off_event->velocity());\n\n    event = RtEvent::make_note_aftertouch_event(124, 3, 2, 48, 0.5f);\n    EXPECT_EQ(RtEventType::NOTE_AFTERTOUCH, event.type());\n    auto note_at_event = event.keyboard_event();\n    EXPECT_EQ(ObjectId(124), note_at_event->processor_id());\n    EXPECT_EQ(3, note_at_event->sample_offset());\n    EXPECT_EQ(2, note_on_event->channel());\n    EXPECT_EQ(48, note_at_event->note());\n    EXPECT_FLOAT_EQ(0.5f, note_at_event->velocity());\n\n    event = RtEvent::make_aftertouch_event(111, 3, 2, 0.6f);\n    EXPECT_EQ(RtEventType::AFTERTOUCH, event.type());\n    auto at_event = event.keyboard_common_event();\n    EXPECT_EQ(ObjectId(111), at_event->processor_id());\n    EXPECT_EQ(3, at_event->sample_offset());\n    EXPECT_EQ(2, note_on_event->channel());\n    EXPECT_FLOAT_EQ(0.6f, at_event->value());\n\n    event = RtEvent::make_pitch_bend_event(112, 4, 3, 0.7f);\n    EXPECT_EQ(RtEventType::PITCH_BEND, event.type());\n    auto pb_event = event.keyboard_common_event();\n    EXPECT_EQ(ObjectId(112), pb_event->processor_id());\n    EXPECT_EQ(4, pb_event->sample_offset());\n    EXPECT_EQ(3, note_on_event->channel());\n    EXPECT_FLOAT_EQ(0.7f, pb_event->value());\n\n    event = RtEvent::make_kb_modulation_event(113, 5, 4, 0.8f);\n    EXPECT_EQ(RtEventType::MODULATION, event.type());\n    auto mod_event = event.keyboard_common_event();\n    EXPECT_EQ(ObjectId(113), mod_event->processor_id());\n    EXPECT_EQ(5, mod_event->sample_offset());\n    EXPECT_EQ(4, note_on_event->channel());\n    EXPECT_FLOAT_EQ(0.8f, mod_event->value());\n\n    event = RtEvent::make_parameter_change_event(125, 4, 64, 0.5f);\n    EXPECT_EQ(RtEventType::FLOAT_PARAMETER_CHANGE, event.type());\n    auto pc_event = event.parameter_change_event();\n    EXPECT_EQ(ObjectId(125), pc_event->processor_id());\n    EXPECT_EQ(4, pc_event->sample_offset());\n    EXPECT_EQ(ObjectId(64), pc_event->param_id());\n    EXPECT_FLOAT_EQ(0.5f, pc_event->value());\n\n    event = RtEvent::make_wrapped_midi_event(126, 5, {6u, 7u, 8u, 0u});\n    EXPECT_EQ(RtEventType::WRAPPED_MIDI_EVENT, event.type());\n    auto wm_event = event.wrapped_midi_event();\n    EXPECT_EQ(ObjectId(126), wm_event->processor_id());\n    EXPECT_EQ(5, wm_event->sample_offset());\n    EXPECT_EQ(6, wm_event->midi_data()[0]);\n    EXPECT_EQ(7, wm_event->midi_data()[1]);\n    EXPECT_EQ(8, wm_event->midi_data()[2]);\n\n    event = RtEvent::make_gate_event(127, 6, 1, true);\n    EXPECT_EQ(RtEventType::GATE_EVENT, event.type());\n    auto gate_event = event.gate_event();\n    EXPECT_EQ(ObjectId(127), gate_event->processor_id());\n    EXPECT_EQ(6, gate_event->sample_offset());\n    EXPECT_EQ(1, gate_event->gate_no());\n    EXPECT_TRUE(gate_event->value());\n\n    event = RtEvent::make_cv_event(128, 7, 2, 0.5f);\n    EXPECT_EQ(RtEventType::CV_EVENT, event.type());\n    auto cv_event = event.cv_event();\n    EXPECT_EQ(ObjectId(128), cv_event->processor_id());\n    EXPECT_EQ(7, cv_event->sample_offset());\n    EXPECT_EQ(2, cv_event->cv_id());\n    EXPECT_FLOAT_EQ(0.5, cv_event->value());\n\n    RtDeletableWrapper<std::string> str(\"Hej\");\n    event = RtEvent::make_string_property_change_event(129, 8, 65, &str);\n    EXPECT_EQ(RtEventType::STRING_PROPERTY_CHANGE, event.type());\n    auto spc_event = event.property_change_event();\n    EXPECT_EQ(ObjectId(129), spc_event->processor_id());\n    EXPECT_EQ(8, spc_event->sample_offset());\n    EXPECT_EQ(ObjectId(65), spc_event->param_id());\n    EXPECT_EQ(\"Hej\", *spc_event->value());\n\n    uint8_t TEST_DATA[3] = {1, 2, 3};\n    BlobData data{sizeof(TEST_DATA), TEST_DATA};\n    event = RtEvent::make_data_property_change_event(130, 9, 66, data);\n    EXPECT_EQ(RtEventType::DATA_PROPERTY_CHANGE, event.type());\n    auto dpc_event = event.data_parameter_change_event();\n    EXPECT_EQ(ObjectId(130), dpc_event->processor_id());\n    EXPECT_EQ(9, dpc_event->sample_offset());\n    EXPECT_EQ(ObjectId(66), dpc_event->param_id());\n    EXPECT_EQ(3, dpc_event->value().data[2]);\n\n    event = RtEvent::make_bypass_processor_event(131, true);\n    EXPECT_EQ(RtEventType::SET_BYPASS, event.type());\n    EXPECT_EQ(131u, event.processor_id());\n    EXPECT_TRUE(event.processor_command_event()->value());\n\n    event = RtEvent::make_add_track_event(121, 2);\n    EXPECT_EQ(RtEventType::ADD_TRACK, event.type());\n    EXPECT_EQ(121, event.track_event()->track());\n    EXPECT_EQ(2, event.track_event()->thread().value());\n\n    event = RtEvent::make_remove_track_event(123);\n    EXPECT_EQ(RtEventType::REMOVE_TRACK, event.type());\n    EXPECT_EQ(123, event.track_event()->track());\n    EXPECT_FALSE(event.track_event()->thread().has_value());\n\n    event = RtEvent::make_insert_processor_event(nullptr);\n    EXPECT_EQ(RtEventType::INSERT_PROCESSOR, event.type());\n    EXPECT_EQ(nullptr, event.processor_operation_event()->instance());\n\n    event = RtEvent::make_remove_processor_event(123u);\n    EXPECT_EQ(RtEventType::REMOVE_PROCESSOR, event.type());\n    EXPECT_EQ(123u, event.processor_reorder_event()->processor());\n    EXPECT_EQ(0u, event.processor_reorder_event()->track());\n\n    event = RtEvent::make_add_processor_to_track_event(ObjectId(123), ObjectId(456), ObjectId(789));\n    EXPECT_EQ(RtEventType::ADD_PROCESSOR_TO_TRACK, event.type());\n    EXPECT_EQ(123u, event.processor_reorder_event()->processor());\n    EXPECT_EQ(456u, event.processor_reorder_event()->track());\n    EXPECT_EQ(789u, event.processor_reorder_event()->before_processor().value_or(-1));\n\n    event = RtEvent::make_remove_processor_from_track_event(ObjectId(123), ObjectId(456));\n    EXPECT_EQ(RtEventType::REMOVE_PROCESSOR_FROM_TRACK, event.type());\n    EXPECT_EQ(123u, event.processor_reorder_event()->processor());\n    EXPECT_EQ(456u, event.processor_reorder_event()->track());\n\n    event = RtEvent::make_tempo_event(25, 130);\n    EXPECT_EQ(RtEventType::TEMPO, event.type());\n    EXPECT_EQ(25, event.tempo_event()->sample_offset());\n    EXPECT_EQ(130.0f, event.tempo_event()->tempo());\n\n    event = RtEvent::make_time_signature_event(26, {7, 8});\n    EXPECT_EQ(RtEventType::TIME_SIGNATURE, event.type());\n    EXPECT_EQ(26, event.time_signature_event()->sample_offset());\n    EXPECT_EQ(7, event.time_signature_event()->time_signature().numerator);\n    EXPECT_EQ(8, event.time_signature_event()->time_signature().denominator);\n\n    event = RtEvent::make_playing_mode_event(27, PlayingMode::PLAYING);\n    EXPECT_EQ(RtEventType::PLAYING_MODE, event.type());\n    EXPECT_EQ(27, event.playing_mode_event()->sample_offset());\n    EXPECT_EQ(PlayingMode::PLAYING, event.playing_mode_event()->mode());\n\n    event = RtEvent::make_sync_mode_event(28, SyncMode::MIDI);\n    EXPECT_EQ(RtEventType::SYNC_MODE, event.type());\n    EXPECT_EQ(28, event.sync_mode_event()->sample_offset());\n    EXPECT_EQ(SyncMode::MIDI, event.sync_mode_event()->mode());\n\n    AudioConnection audio_con = {123, 345, 24};\n    event = RtEvent::make_add_audio_input_connection_event(audio_con);\n    EXPECT_EQ(RtEventType::ADD_AUDIO_CONNECTION, event.type());\n    EXPECT_TRUE(event.audio_connection_event()->input_connection());\n    EXPECT_FALSE(event.audio_connection_event()->output_connection());\n    EXPECT_EQ(audio_con.track, event.audio_connection_event()->connection().track);\n    EXPECT_EQ(audio_con.track_channel, event.audio_connection_event()->connection().track_channel);\n    EXPECT_EQ(audio_con.engine_channel, event.audio_connection_event()->connection().engine_channel);\n\n    event = RtEvent::make_add_audio_output_connection_event(audio_con);\n    EXPECT_EQ(RtEventType::ADD_AUDIO_CONNECTION, event.type());\n    EXPECT_FALSE(event.audio_connection_event()->input_connection());\n    EXPECT_TRUE(event.audio_connection_event()->output_connection());\n\n    event = RtEvent::make_remove_audio_input_connection_event(audio_con);\n    EXPECT_EQ(RtEventType::REMOVE_AUDIO_CONNECTION, event.type());\n    EXPECT_TRUE(event.audio_connection_event()->input_connection());\n\n    event = RtEvent::make_remove_audio_output_connection_event(audio_con);\n    EXPECT_EQ(RtEventType::REMOVE_AUDIO_CONNECTION, event.type());\n    EXPECT_TRUE(event.audio_connection_event()->output_connection());\n\n    CvConnection cv_con = {123, 345, 24};\n    event = RtEvent::make_add_cv_input_connection_event(cv_con);\n    EXPECT_EQ(RtEventType::ADD_CV_CONNECTION, event.type());\n    EXPECT_TRUE(event.cv_connection_event()->input_connection());\n    EXPECT_FALSE(event.cv_connection_event()->output_connection());\n    EXPECT_EQ(cv_con.cv_id, event.cv_connection_event()->connection().cv_id);\n    EXPECT_EQ(cv_con.parameter_id, event.cv_connection_event()->connection().parameter_id);\n    EXPECT_EQ(cv_con.processor_id, event.cv_connection_event()->connection().processor_id);\n\n    event = RtEvent::make_add_cv_output_connection_event(cv_con);\n    EXPECT_EQ(RtEventType::ADD_CV_CONNECTION, event.type());\n    EXPECT_TRUE(event.cv_connection_event()->output_connection());\n\n    event = RtEvent::make_remove_cv_input_connection_event(cv_con);\n    EXPECT_EQ(RtEventType::REMOVE_CV_CONNECTION, event.type());\n    EXPECT_TRUE(event.cv_connection_event()->input_connection());\n\n    event = RtEvent::make_remove_cv_output_connection_event(cv_con);\n    EXPECT_EQ(RtEventType::REMOVE_CV_CONNECTION, event.type());\n    EXPECT_TRUE(event.cv_connection_event()->output_connection());\n\n    GateConnection gate_con = {12, 34, 24, 78};\n    event = RtEvent::make_add_gate_input_connection_event(gate_con);\n    EXPECT_EQ(RtEventType::ADD_GATE_CONNECTION, event.type());\n    EXPECT_TRUE(event.gate_connection_event()->input_connection());\n    EXPECT_FALSE(event.gate_connection_event()->output_connection());\n    EXPECT_EQ(gate_con.processor_id, event.gate_connection_event()->connection().processor_id);\n    EXPECT_EQ(gate_con.channel, event.gate_connection_event()->connection().channel);\n    EXPECT_EQ(gate_con.gate_id, event.gate_connection_event()->connection().gate_id);\n    EXPECT_EQ(gate_con.note_no, event.gate_connection_event()->connection().note_no);\n\n    event = RtEvent::make_add_gate_output_connection_event(gate_con);\n    EXPECT_EQ(RtEventType::ADD_GATE_CONNECTION, event.type());\n    EXPECT_TRUE(event.gate_connection_event()->output_connection());\n\n    event = RtEvent::make_remove_gate_input_connection_event(gate_con);\n    EXPECT_EQ(RtEventType::REMOVE_GATE_CONNECTION, event.type());\n    EXPECT_TRUE(event.gate_connection_event()->input_connection());\n\n    event = RtEvent::make_remove_gate_output_connection_event(gate_con);\n    EXPECT_EQ(RtEventType::REMOVE_GATE_CONNECTION, event.type());\n    EXPECT_TRUE(event.gate_connection_event()->output_connection());\n\n    event = RtEvent::make_timing_tick_event(29, 12);\n    EXPECT_EQ(RtEventType::TIMING_TICK, event.type());\n    EXPECT_EQ(29, event.timing_tick_event()->sample_offset());\n    EXPECT_EQ(12, event.timing_tick_event()->tick_count());\n\n    event = RtEvent::make_processor_notify_event(30, ProcessorNotifyRtEvent::Action::PARAMETER_UPDATE);\n    EXPECT_EQ(RtEventType::NOTIFY, event.type());\n    EXPECT_EQ(ProcessorNotifyRtEvent::Action::PARAMETER_UPDATE, event.processor_notify_event()->action());\n}\n\nTEST (TestRealtimeEvents, TestMidiFactoryFunction)\n{\n    midi::NoteOnMessage note_on{.channel = 0, .note = 65, .velocity = 127};\n\n    auto event = RtEvent::make_note_on_event(123, 1, note_on);\n    EXPECT_EQ(RtEventType::NOTE_ON, event.type());\n    auto note_on_event = event.keyboard_event();\n    EXPECT_EQ(ObjectId(123), note_on_event->processor_id());\n    EXPECT_EQ(1, note_on_event->sample_offset());\n    EXPECT_EQ(0, note_on_event->channel());\n    EXPECT_EQ(65, note_on_event->note());\n    EXPECT_FLOAT_EQ(1.0, note_on_event->velocity());\n\n    midi::NoteOffMessage note_off{.channel = 1, .note = 66, .velocity = 127};\n\n    event = RtEvent::make_note_off_event(122, 2, note_off);\n    EXPECT_EQ(RtEventType::NOTE_OFF, event.type());\n    auto note_off_event = event.keyboard_event();\n    EXPECT_EQ(ObjectId(122), note_off_event->processor_id());\n    EXPECT_EQ(2, note_off_event->sample_offset());\n    EXPECT_EQ(1, note_on_event->channel());\n    EXPECT_EQ(66, note_off_event->note());\n    EXPECT_FLOAT_EQ(1.0, note_off_event->velocity());\n\n    midi::PolyKeyPressureMessage poly_press{.channel = 2, .note = 67, .pressure = 127};\n\n    event = RtEvent::make_note_aftertouch_event(124, 3, poly_press);\n    EXPECT_EQ(RtEventType::NOTE_AFTERTOUCH, event.type());\n    auto note_at_event = event.keyboard_event();\n    EXPECT_EQ(ObjectId(124), note_at_event->processor_id());\n    EXPECT_EQ(3, note_at_event->sample_offset());\n    EXPECT_EQ(2, note_on_event->channel());\n    EXPECT_EQ(67, note_at_event->note());\n    EXPECT_FLOAT_EQ(1.0, note_at_event->velocity());\n\n    midi::ChannelPressureMessage ch_press{.channel = 3, .pressure = 127};\n\n    event = RtEvent::make_aftertouch_event(111, 4, ch_press);\n    EXPECT_EQ(RtEventType::AFTERTOUCH, event.type());\n    auto at_event = event.keyboard_common_event();\n    EXPECT_EQ(ObjectId(111), at_event->processor_id());\n    EXPECT_EQ(4, at_event->sample_offset());\n    EXPECT_EQ(3, note_on_event->channel());\n    EXPECT_FLOAT_EQ(1.0, at_event->value());\n\n    midi::PitchBendMessage pitch_bnd{.channel = 4, .value = midi::PITCH_BEND_MIDDLE / 2};\n\n    event = RtEvent::make_pitch_bend_event(112, 5, pitch_bnd);\n    EXPECT_EQ(RtEventType::PITCH_BEND, event.type());\n    auto pb_event = event.keyboard_common_event();\n    EXPECT_EQ(ObjectId(112), pb_event->processor_id());\n    EXPECT_EQ(5, pb_event->sample_offset());\n    EXPECT_EQ(4, note_on_event->channel());\n    EXPECT_FLOAT_EQ(-0.5, pb_event->value());\n\n    midi::ControlChangeMessage mod{.channel = 5, .controller = midi::MOD_WHEEL_CONTROLLER_NO, .value = 127};\n\n    event = RtEvent::make_kb_modulation_event(113, 6, mod);\n    EXPECT_EQ(RtEventType::MODULATION, event.type());\n    auto mod_event = event.keyboard_common_event();\n    EXPECT_EQ(ObjectId(113), mod_event->processor_id());\n    EXPECT_EQ(6, mod_event->sample_offset());\n    EXPECT_EQ(5, note_on_event->channel());\n    EXPECT_FLOAT_EQ(1.0f, mod_event->value());\n}\n\nTEST(TestRealtimeEvents, TestReturnableEvents)\n{\n    auto event = RtEvent::make_insert_processor_event(nullptr);\n    auto event2 = RtEvent::make_insert_processor_event(nullptr);\n    auto typed_event = event.returnable_event();\n    /* Assert that 2 events don't share the same id */\n    EXPECT_NE(event2.returnable_event()->event_id(), typed_event->event_id());\n    /* Verify handling logic */\n    EXPECT_EQ(ReturnableRtEvent::EventStatus::UNHANDLED, typed_event->status());\n    typed_event->set_handled(true);\n    EXPECT_EQ(ReturnableRtEvent::EventStatus::HANDLED_OK, typed_event->status());\n    typed_event->set_handled(false);\n    EXPECT_EQ(ReturnableRtEvent::EventStatus::HANDLED_ERROR, typed_event->status());\n}\n\nTEST(TestRealtimeEvents, TestIsKeyboardEvent)\n{\n    auto event = RtEvent::make_parameter_change_event(1, 2, 3, 1.0f);\n    EXPECT_FALSE(is_keyboard_event(event));\n    event = RtEvent::make_note_off_event(1, 2, 0, 3, 1.0f);\n    EXPECT_TRUE(is_keyboard_event(event));\n    event = RtEvent::make_wrapped_midi_event(1, 2, {0, 0 ,0, 0});\n    EXPECT_TRUE(is_keyboard_event(event));\n}\n\n"
  },
  {
    "path": "test/unittests/library/sample_buffer_test.cpp",
    "content": "#include <algorithm>\n#include <array>\n#include \"gtest/gtest.h\"\n\n#include \"sushi/sample_buffer.h\"\n#include \"test_utils/test_utils.h\"\n\nusing namespace sushi;\n\nTEST(TestSampleBuffer, TestCopying)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> test_buffer(4);\n    float* data = test_buffer.channel(0);\n    std::fill(data, data + AUDIO_CHUNK_SIZE, 2.0f);\n    SampleBuffer<AUDIO_CHUNK_SIZE> copy_buffer(test_buffer);\n    EXPECT_EQ(test_buffer.channel_count(), copy_buffer.channel_count());\n    EXPECT_FLOAT_EQ(test_buffer.channel(0)[10], copy_buffer.channel(0)[10]);\n    EXPECT_NE(test_buffer.channel(0), copy_buffer.channel(0));\n\n    // When copy constructing from r-value, the original data should be preserved in a new container\n    SampleBuffer<AUDIO_CHUNK_SIZE> r_value_copy(std::move(test_buffer));\n    EXPECT_EQ(copy_buffer.channel_count(), r_value_copy.channel_count());\n    EXPECT_FLOAT_EQ(copy_buffer.channel(0)[10], r_value_copy.channel(0)[10]);\n    EXPECT_NE(copy_buffer.channel(0), r_value_copy.channel(0));\n}\n\nTEST(TestSampleBuffer, TestAssignement)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> test_buffer(4);\n    float* data = test_buffer.channel(0);\n    std::fill(data, data + AUDIO_CHUNK_SIZE * 4, 2.0f);\n    SampleBuffer<AUDIO_CHUNK_SIZE> copy_buffer = test_buffer;\n\n    EXPECT_EQ(test_buffer.channel_count(), copy_buffer.channel_count());\n    EXPECT_FLOAT_EQ(test_buffer.channel(0)[10], copy_buffer.channel(0)[10]);\n    EXPECT_NE(test_buffer.channel(0), copy_buffer.channel(0));\n\n    // Test assigment that involves reallocation (from 4 channels to 2)\n    SampleBuffer<AUDIO_CHUNK_SIZE> test_buffer_2(2);\n    data = test_buffer_2.channel(0);\n    std::fill(data, data + AUDIO_CHUNK_SIZE * 2, 3.0f);\n    copy_buffer = test_buffer_2;\n\n    EXPECT_EQ(test_buffer_2.channel_count(), copy_buffer.channel_count());\n    EXPECT_FLOAT_EQ(test_buffer_2.channel(0)[10], copy_buffer.channel(0)[10]);\n    EXPECT_NE(test_buffer_2.channel(0), copy_buffer.channel(0));\n\n    // Test move assigment, the original data should be preserved in a new container\n    data = test_buffer.channel(0);\n    SampleBuffer<AUDIO_CHUNK_SIZE> move_copy = std::move(test_buffer);\n    EXPECT_EQ(4, move_copy.channel_count());\n    EXPECT_FLOAT_EQ(2.0f, move_copy.channel(0)[10]);\n    EXPECT_EQ(data, move_copy.channel(0));\n\n    // Test assignment of empty buffers\n    SampleBuffer<AUDIO_CHUNK_SIZE> empty_buffer;\n    SampleBuffer<AUDIO_CHUNK_SIZE> empty_test_buffer = empty_buffer;\n\n    EXPECT_EQ(empty_buffer.channel_count(), empty_test_buffer.channel_count());\n    EXPECT_EQ(nullptr, empty_buffer.channel(0));\n\n    // Assign empty buffer to non-empty buffer\n    SampleBuffer<AUDIO_CHUNK_SIZE> test_buffer_3(2);\n    test_buffer_3 = empty_buffer;\n    EXPECT_EQ(0, test_buffer_3.channel_count());\n    EXPECT_EQ(nullptr, test_buffer_3.channel(0));\n\n}\n\nTEST(TestSampleBuffer, TestNonOwningBuffer)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> test_buffer(4);\n    float* data = test_buffer.channel(0);\n    std::fill(data, data + AUDIO_CHUNK_SIZE * 2, 2.0f);\n    std::fill(data + AUDIO_CHUNK_SIZE * 2, data + AUDIO_CHUNK_SIZE * 4, 4.0f);\n    {\n        // Create a non owning buffer and assert that is wraps the same data\n        // And doesn't destroy the data when it goes out of scope\n        SampleBuffer<AUDIO_CHUNK_SIZE> non_owning_buffer = SampleBuffer<AUDIO_CHUNK_SIZE>::create_non_owning_buffer(test_buffer, 0, 2);\n        test_utils::assert_buffer_value(2.0f, non_owning_buffer);\n        non_owning_buffer = SampleBuffer<AUDIO_CHUNK_SIZE>::create_non_owning_buffer(test_buffer, 2, 2);\n        test_utils::assert_buffer_value(4.0f, non_owning_buffer);\n        // Excercise assignment and move contructors\n        SampleBuffer<AUDIO_CHUNK_SIZE> new_buffer(2);\n        SampleBuffer<AUDIO_CHUNK_SIZE> new_buffer_2;\n        new_buffer = non_owning_buffer;\n        new_buffer_2 = std::move(non_owning_buffer);\n    }\n    // Touch the sample data to provoke a crash if it was accidentally deleted\n    EXPECT_FLOAT_EQ(2.0f, *test_buffer.channel(1));\n}\n\nTEST(TestSampleBuffer, TestCreateFromRawPointer)\n{\n    std::array<float, 2 * AUDIO_CHUNK_SIZE> raw_data;\n    std::fill(raw_data.begin(), raw_data.begin() + AUDIO_CHUNK_SIZE, 2.0f);\n    std::fill(raw_data.begin() + AUDIO_CHUNK_SIZE, raw_data.end(), 4.0f);\n\n    auto test_buffer = SampleBuffer<AUDIO_CHUNK_SIZE>::create_from_raw_pointer(raw_data.data(), 0, 2);\n    EXPECT_EQ(2, test_buffer.channel_count());\n    EXPECT_FLOAT_EQ(2.0, test_buffer.channel(0)[0]);\n    EXPECT_FLOAT_EQ(4.0, test_buffer.channel(1)[0]);\n\n    test_buffer = SampleBuffer<AUDIO_CHUNK_SIZE>::create_from_raw_pointer(raw_data.data(), 1, 1);\n    EXPECT_EQ(1, test_buffer.channel_count());\n    EXPECT_FLOAT_EQ(4.0, test_buffer.channel(0)[0]);\n}\n\n\nTEST(TestSampleBuffer, TestAssigningNonOwningBuffer)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> test_buffer_1(2);\n    SampleBuffer<AUDIO_CHUNK_SIZE> test_buffer_2(2);\n\n    float* data = test_buffer_1.channel(0);\n    std::fill(data, data + AUDIO_CHUNK_SIZE * 2, 2.0f);\n    test_buffer_2.clear();\n    {\n        // Create 2 non-owning buffers and assign one to the other\n        SampleBuffer<AUDIO_CHUNK_SIZE> no_buffer_1 = SampleBuffer<AUDIO_CHUNK_SIZE>::create_non_owning_buffer(test_buffer_1, 0, 2);\n        SampleBuffer<AUDIO_CHUNK_SIZE> no_buffer_2 = SampleBuffer<AUDIO_CHUNK_SIZE>::create_non_owning_buffer(test_buffer_2, 0, 2);\n        test_utils::assert_buffer_value(2.0f, no_buffer_1);\n\n        no_buffer_2 = no_buffer_1;\n        test_utils::assert_buffer_value(2.0f, no_buffer_2);\n        test_utils::assert_buffer_value(2.0f, test_buffer_2);\n    }\n    {\n        // Copy construct 2 non-owning buffers and assign one to the other\n        SampleBuffer<AUDIO_CHUNK_SIZE> no_buffer_1{SampleBuffer<AUDIO_CHUNK_SIZE>::create_non_owning_buffer(test_buffer_1, 0, 2)};\n        SampleBuffer<AUDIO_CHUNK_SIZE> no_buffer_2{SampleBuffer<AUDIO_CHUNK_SIZE>::create_non_owning_buffer(test_buffer_2, 0, 2)};\n        test_utils::assert_buffer_value(2.0f, no_buffer_1);\n\n        no_buffer_2.clear();\n        no_buffer_2 = no_buffer_1;\n        test_utils::assert_buffer_value(2.0f, no_buffer_2);\n        test_utils::assert_buffer_value(2.0f, test_buffer_2);\n    }\n    // Touch the sample data to provoke a crash if it was accidentally deleted\n    EXPECT_FLOAT_EQ(2.0f, *test_buffer_2.channel(1));\n}\n\nTEST(TestSampleBuffer, TestSwap)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer_1(2);\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer_2(1);\n    std::fill(buffer_1.channel(0), buffer_1.channel(0) + AUDIO_CHUNK_SIZE, 2.0f);\n\n    swap(buffer_1, buffer_2);\n\n    EXPECT_EQ(1, buffer_1.channel_count());\n    EXPECT_EQ(2, buffer_2.channel_count());\n    EXPECT_FLOAT_EQ(0.0f, buffer_1.channel(0)[0]);\n    EXPECT_FLOAT_EQ(2.0f, buffer_2.channel(0)[0]);\n}\n\nTEST(TestSampleBuffer, TestInitialization)\n{\n    SampleBuffer<2> buffer(42);\n    EXPECT_EQ(42, buffer.channel_count());\n    SampleBuffer<3> buffer2;\n    EXPECT_EQ(0, buffer2.channel_count());\n    EXPECT_EQ(nullptr, buffer2.channel(0));\n}\n\nTEST(TestSampleBuffer, TestDeinterleaving)\n{\n    float interleaved_buffer[6] = {1, 2, 1, 2, 1, 2};\n    SampleBuffer<3> buffer(2);\n    buffer.from_interleaved(interleaved_buffer);\n    for (unsigned int n = 0; n < 3; ++n)\n    {\n        ASSERT_FLOAT_EQ(1.0f, buffer.channel(0)[n]);\n        ASSERT_FLOAT_EQ(2.0f, buffer.channel(1)[n]);\n    }\n\n    float interleaved_3_ch[9] = {1, 2, 3, 1, 2, 3, 1, 2, 3};\n    SampleBuffer<3> buffer_3ch(3);\n    buffer_3ch.from_interleaved(interleaved_3_ch);\n    for (unsigned int n = 0; n < 3; ++n)\n    {\n        ASSERT_FLOAT_EQ(1.0f, buffer_3ch.channel(0)[n]);\n        ASSERT_FLOAT_EQ(2.0f, buffer_3ch.channel(1)[n]);\n        ASSERT_FLOAT_EQ(3.0f, buffer_3ch.channel(2)[n]);\n    }\n}\n\nTEST(TestSampleBuffer, TestInterleaving)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer(2);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        buffer.channel(0)[n] = 0.0f;\n        buffer.channel(1)[n] = 1.0f;\n    }\n    float interleaved_buffer[AUDIO_CHUNK_SIZE * 2];\n    buffer.to_interleaved(interleaved_buffer);\n    for (unsigned int n = 0; n < (AUDIO_CHUNK_SIZE * 2) ; n+=2)\n    {\n        ASSERT_FLOAT_EQ(0.0f, interleaved_buffer[n]);\n        ASSERT_FLOAT_EQ(1.0f, interleaved_buffer[n+1]);\n    }\n\n    float interleaved_3ch[AUDIO_CHUNK_SIZE * 3];\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer_3ch(3);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        buffer_3ch.channel(0)[n] = 0.5f;\n        buffer_3ch.channel(1)[n] = 1.0f;\n        buffer_3ch.channel(2)[n] = 2.0f;\n    }\n    buffer_3ch.to_interleaved(interleaved_3ch);\n    for (unsigned int n = 0; n < (AUDIO_CHUNK_SIZE * 3); n+=3)\n    {\n        ASSERT_FLOAT_EQ(0.5f, interleaved_3ch[n]);\n        ASSERT_FLOAT_EQ(1.0f, interleaved_3ch[n+1]);\n        ASSERT_FLOAT_EQ(2.0f, interleaved_3ch[n+2]);\n    }\n}\n\n\nTEST (TestSampleBuffer, TestGain)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer(2);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        buffer.channel(0)[n] = 2.0f;\n        buffer.channel(1)[n] = 3.0f;\n    }\n\n    // Test gain\n    buffer.apply_gain(2.0f);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        ASSERT_FLOAT_EQ(4.0f, buffer.channel(0)[n]);\n        ASSERT_FLOAT_EQ(6.0f, buffer.channel(1)[n]);\n    }\n\n    // Test per-channel gain\n    buffer.apply_gain(1.5f, 0);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        ASSERT_FLOAT_EQ(6.0f, buffer.channel(0)[n]);\n        ASSERT_FLOAT_EQ(6.0f, buffer.channel(1)[n]);\n    }\n}\n\nTEST(TestSampleBuffer,TestReplace)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer_1(2);\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer_2(2);\n    test_utils::fill_sample_buffer(buffer_1, 1.0f);\n    test_utils::fill_sample_buffer(buffer_2, 2.0f);\n\n    // copy ch 1 of buffer 2 to ch 0 of buffer_1\n    buffer_1.replace(0, 1, buffer_2);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        ASSERT_FLOAT_EQ(2.0f, buffer_1.channel(0)[n]);\n        ASSERT_FLOAT_EQ(1.0f, buffer_1.channel(1)[n]);\n    }\n    // copy all channels of buffer 2 to buffer 1\n    buffer_1.replace(buffer_2);\n    test_utils::assert_buffer_value(2.0f, buffer_1);\n}\n\nTEST (TestSampleBuffer, TestAdd)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer(2);\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer_2(2);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        buffer.channel(0)[n] = 2.0f;\n        buffer.channel(1)[n] = 3.0f;\n        buffer_2.channel(0)[n] = 1.0f;\n        buffer_2.channel(1)[n] = 1.0f;\n    }\n    // Test buffers with equal channel count\n    buffer.add(buffer_2);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        ASSERT_FLOAT_EQ(3.0f, buffer.channel(0)[n]);\n        ASSERT_FLOAT_EQ(4.0f, buffer.channel(1)[n]);\n    }\n\n    // Test adding mono buffer to stereo buffer\n    SampleBuffer<AUDIO_CHUNK_SIZE> mono_buffer(1);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        mono_buffer.channel(0)[n] = 2.0f;\n    }\n\n    buffer.add(mono_buffer);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        ASSERT_FLOAT_EQ(5.0f, buffer.channel(0)[n]);\n        ASSERT_FLOAT_EQ(6.0f, buffer.channel(1)[n]);\n    }\n}\n\nTEST (TestSampleBuffer, TestAddWithGain)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer(2);\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer_2(2);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        buffer.channel(0)[n] = 2.0f;\n        buffer.channel(1)[n] = 3.0f;\n        buffer_2.channel(0)[n] = 1.0f;\n        buffer_2.channel(1)[n] = 1.0f;\n    }\n    // Test buffers with equal channel count\n    buffer.add_with_gain(buffer_2, 2.0f);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        ASSERT_FLOAT_EQ(4.0f, buffer.channel(0)[n]);\n        ASSERT_FLOAT_EQ(5.0f, buffer.channel(1)[n]);\n    }\n\n    // Test adding mono buffer to stereo buffer\n    SampleBuffer<AUDIO_CHUNK_SIZE> mono_buffer(1);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        mono_buffer.channel(0)[n] = 2.0f;\n    }\n\n    buffer.add_with_gain(mono_buffer, 1.5f);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        ASSERT_FLOAT_EQ(7.0f, buffer.channel(0)[n]);\n        ASSERT_FLOAT_EQ(8.0f, buffer.channel(1)[n]);\n    }\n\n    // Test single channel adding with gain\n    buffer.add_with_gain(1, 1, buffer_2, -2.0f);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        ASSERT_FLOAT_EQ(7.0f, buffer.channel(0)[n]);\n        ASSERT_FLOAT_EQ(6.0f, buffer.channel(1)[n]);\n    }\n}\n\nTEST (TestSampleBuffer, TestRamping)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer(2);\n    for (unsigned int i = 0; i < AUDIO_CHUNK_SIZE; ++i)\n    {\n        buffer.channel(0)[i] = 1.0f;\n        buffer.channel(1)[i] = 1.0f;\n    }\n    buffer.ramp(1, 2);\n    ASSERT_FLOAT_EQ(1.0f, buffer.channel(0)[0]);\n    ASSERT_FLOAT_EQ(1.0f, buffer.channel(1)[0]);\n\n    ASSERT_NEAR(2.0f, buffer.channel(0)[AUDIO_CHUNK_SIZE - 1], 0.0001f);\n    ASSERT_NEAR(2.0f, buffer.channel(1)[AUDIO_CHUNK_SIZE - 1], 0.0001f);\n\n    ASSERT_NEAR(1.5f, buffer.channel(0)[AUDIO_CHUNK_SIZE / 2], 0.05f);\n    ASSERT_NEAR(1.5f, buffer.channel(1)[AUDIO_CHUNK_SIZE / 2], 0.05f);\n\n    buffer.ramp_down();\n    ASSERT_FLOAT_EQ(1.0f, buffer.channel(0)[0]);\n    ASSERT_FLOAT_EQ(1.0f, buffer.channel(1)[0]);\n    ASSERT_NEAR(0.0f, buffer.channel(0)[AUDIO_CHUNK_SIZE - 1], 0.0001f);\n    ASSERT_NEAR(0.0f, buffer.channel(1)[AUDIO_CHUNK_SIZE - 1], 0.0001f);\n}\n\nTEST (TestSampleBuffer, TestAddWithRamp)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer(2);\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer_2(2);\n    for (unsigned int i = 0; i < AUDIO_CHUNK_SIZE; ++i)\n    {\n        buffer.channel(0)[i] = 1.0f;\n        buffer.channel(1)[i] = 1.0f;\n        buffer_2.channel(0)[i] = 1.0f;\n        buffer_2.channel(1)[i] = 1.0f;\n    }\n\n    // Test buffers with equal channel count\n    buffer.add_with_ramp(buffer_2, 0.5f, 1.0f);\n\n    ASSERT_FLOAT_EQ(1.5f, buffer.channel(0)[0]);\n    ASSERT_FLOAT_EQ(1.5f, buffer.channel(1)[0]);\n\n    ASSERT_NEAR(1.75f, buffer.channel(0)[AUDIO_CHUNK_SIZE / 2 - 1], 0.05f);\n    ASSERT_NEAR(1.75f, buffer.channel(1)[AUDIO_CHUNK_SIZE / 2 - 1], 0.05f);\n\n    ASSERT_FLOAT_EQ(2.0f, buffer.channel(0)[AUDIO_CHUNK_SIZE - 1]);\n    ASSERT_FLOAT_EQ(2.0f, buffer.channel(1)[AUDIO_CHUNK_SIZE - 1]);\n\n    // Test adding mono buffer to stereo buffer with ramp\n    SampleBuffer<AUDIO_CHUNK_SIZE> mono_buffer(1);\n    for (unsigned int n = 0; n < AUDIO_CHUNK_SIZE; ++n)\n    {\n        mono_buffer.channel(0)[n] = 1.0f;\n    }\n    for (unsigned int i = 0; i < AUDIO_CHUNK_SIZE; ++i)\n    {\n        buffer.channel(0)[i] = 1.0f;\n        buffer.channel(1)[i] = 1.0f;\n    }\n\n    buffer.add_with_ramp(mono_buffer, 1.0f, 2.0f);\n\n    ASSERT_FLOAT_EQ(2.0f, buffer.channel(0)[0]);\n    ASSERT_FLOAT_EQ(2.0f, buffer.channel(1)[0]);\n\n    ASSERT_NEAR(2.5f, buffer.channel(0)[AUDIO_CHUNK_SIZE / 2 - 1], 0.05f);\n    ASSERT_NEAR(2.5f, buffer.channel(1)[AUDIO_CHUNK_SIZE / 2 - 1], 0.05f);\n\n    ASSERT_FLOAT_EQ(3.0f, buffer.channel(0)[AUDIO_CHUNK_SIZE - 1]);\n    ASSERT_FLOAT_EQ(3.0f, buffer.channel(1)[AUDIO_CHUNK_SIZE - 1]);\n}\n\nTEST (TestSampleBuffer, TestCountClippedSamples)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer(2);\n    ASSERT_EQ(0, buffer.count_clipped_samples(0));\n\n    buffer.channel(0)[4] = 1.7f;\n    buffer.channel(1)[3] = 1.1f;\n    buffer.channel(1)[2] = -1.05f;\n    EXPECT_EQ(1, buffer.count_clipped_samples(0));\n    EXPECT_EQ(2, buffer.count_clipped_samples(1));\n}\n\nTEST (TestSampleBuffer, TestPeakCalculation)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer(2);\n    ASSERT_FLOAT_EQ(0, buffer.calc_peak_value(0));\n\n    buffer.channel(0)[4] = 0.5f;\n    buffer.channel(1)[3] = 1.1f;\n    buffer.channel(1)[2] = -1.5f;\n    EXPECT_FLOAT_EQ(0.5, buffer.calc_peak_value(0));\n    EXPECT_FLOAT_EQ(1.5, buffer.calc_peak_value(1));\n}\n\nTEST (TestSampleBuffer, TestRMSCalculation)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> buffer(2);\n    ASSERT_FLOAT_EQ(0, buffer.calc_rms_value(0));\n\n    // Fill channel 0 with a square wave and channel 1 with a sine wave\n    for (int i = 0; i < AUDIO_CHUNK_SIZE; ++i)\n    {\n        buffer.channel(0)[i] = i % 2 == 0? 1.0f : -1.0f;\n        buffer.channel(1)[i] = sinf(i * 0.5f);\n    }\n\n    EXPECT_FLOAT_EQ(1, buffer.calc_rms_value(0));\n    EXPECT_NEAR(1.0f / std::sqrt(2), buffer.calc_rms_value(1), 0.01);\n}\n"
  },
  {
    "path": "test/unittests/library/simple_fifo_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"library/simple_fifo.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\n\nconstexpr int FIFO_SIZE = 5;\n\nclass TestSimpleFifo : public ::testing::Test\n{\nprotected:\n    TestSimpleFifo() {}\n\n    SimpleFifo<int, FIFO_SIZE + 1> _module_under_test;\n};\n\nTEST_F(TestSimpleFifo, TestOperation)\n{\n    EXPECT_TRUE(_module_under_test.empty());\n\n    for (int i = 0; i < FIFO_SIZE; ++i)\n    {\n        EXPECT_TRUE(_module_under_test.push(i));\n        EXPECT_FALSE(_module_under_test.empty());\n    }\n    // Queue should now be full\n    ASSERT_FALSE(_module_under_test.push(10));\n\n    // Spot checks\n    ASSERT_EQ(2, _module_under_test[2]);\n    ASSERT_EQ(4, _module_under_test[4]);\n\n    int val = 0;\n    while (_module_under_test.empty() == false)\n    {\n        const auto& i = _module_under_test.pop();\n        ASSERT_EQ(val, i);\n        val++;\n    }\n\n    ASSERT_TRUE(_module_under_test.empty());\n}\n\nTEST_F(TestSimpleFifo, TestClear)\n{\n    for (int i = 0; i < FIFO_SIZE; ++i)\n    {\n        EXPECT_TRUE(_module_under_test.push(i));\n        EXPECT_FALSE(_module_under_test.empty());\n    }\n    _module_under_test.clear();\n    EXPECT_TRUE(_module_under_test.empty());\n}\n\nTEST_F(TestSimpleFifo, TestPopAndPush)\n{\n    for (int i = 0; i < FIFO_SIZE; ++i)\n    {\n        EXPECT_TRUE(_module_under_test.push(i));\n    }\n    int value = 12345;\n    EXPECT_TRUE(_module_under_test.pop(value));\n    EXPECT_EQ(0, value);\n    EXPECT_TRUE(_module_under_test.pop(value));\n    EXPECT_EQ(1, value);\n\n    // Push 2 more, should wrap around\n    EXPECT_TRUE(_module_under_test.push(FIFO_SIZE));\n    EXPECT_TRUE(_module_under_test.push(FIFO_SIZE + 1));\n\n    int i = 2;\n    EXPECT_EQ(2, _module_under_test[0]);\n    // empty buffer and check that values are popped in order\n    while (_module_under_test.pop(value))\n    {\n        EXPECT_EQ(i, value);\n        i++;\n    }\n    EXPECT_TRUE(_module_under_test.empty());\n}"
  },
  {
    "path": "test/unittests/library/vst2x_midi_event_fifo_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"library/vst2x/vst2x_midi_event_fifo.h\"\n\nusing namespace sushi::internal;\nusing namespace sushi::internal::vst2;\n\nnamespace {\n    constexpr int TEST_FIFO_CAPACITY = 128;\n    constexpr int TEST_DATA_SIZE = 100;\n} // anonymous namespace\n\nclass TestVst2xMidiEventFIFO : public ::testing::Test\n{\nprotected:\n    TestVst2xMidiEventFIFO() = default;\n\n    void SetUp() override\n    {\n        // Pre-fill queue\n        for (int i = 0; i < TEST_DATA_SIZE; i++)\n        {\n            auto ev = RtEvent::make_note_on_event(0, i, 0, 0, 1.0f);\n            ASSERT_EQ(true, _module_under_test.push(ev));\n        }\n    }\n\n    Vst2xMidiEventFIFO<TEST_FIFO_CAPACITY> _module_under_test;\n};\n\nTEST_F(TestVst2xMidiEventFIFO, TestNonOverflowingBehaviour)\n{\n\n    auto vst_events = _module_under_test.flush();\n    ASSERT_EQ(TEST_DATA_SIZE, vst_events->numEvents);\n    for (int i=0; i<TEST_DATA_SIZE; i++)\n    {\n        auto midi_ev = reinterpret_cast<VstMidiEvent*>(vst_events->events[i]);\n        EXPECT_EQ(i, midi_ev->deltaFrames);\n    }\n}\n\nTEST_F(TestVst2xMidiEventFIFO, TestFlush)\n{\n    _module_under_test.flush();\n    auto vst_events = _module_under_test.flush();\n    EXPECT_EQ(0, vst_events->numEvents);\n}\n\nTEST_F(TestVst2xMidiEventFIFO, TestOverflow)\n{\n    constexpr int overflow_offset = 1000;\n\n    for (int i=TEST_DATA_SIZE; i<TEST_FIFO_CAPACITY; i++)\n    {\n        auto ev = RtEvent::make_note_on_event(0, i, 0, 0, 1.0f);\n        ASSERT_TRUE(_module_under_test.push(ev));\n    }\n    for (int i=0; i<TEST_DATA_SIZE; i++)\n    {\n        auto ev = RtEvent::make_note_on_event(0, overflow_offset+i, 0, 0, 1.0f);\n        ASSERT_FALSE(_module_under_test.push(ev));\n    }\n    auto vst_events = _module_under_test.flush();\n    ASSERT_EQ(TEST_FIFO_CAPACITY, vst_events->numEvents);\n    for (int i=0; i<TEST_DATA_SIZE; i++)\n    {\n        auto midi_ev = reinterpret_cast<VstMidiEvent*>(vst_events->events[i]);\n        EXPECT_EQ(overflow_offset + i, midi_ev->deltaFrames);\n    }\n}\n\nTEST_F(TestVst2xMidiEventFIFO, TestFlushAfterOverflow)\n{\n    // Let the queue overflow...\n    for (int i=0; i<(2*TEST_FIFO_CAPACITY); i++)\n    {\n        auto ev = RtEvent::make_note_on_event(0, i, 0, 0, 1.0f);\n        _module_under_test.push(ev);\n    }\n    _module_under_test.flush();\n\n    // ... and check that after flushing is working again in normal, non-overflowed conditions\n    for (int i=0; i<TEST_DATA_SIZE; i++)\n    {\n        auto ev = RtEvent::make_note_on_event(0, i, 0, 0, 1.0f);\n        ASSERT_EQ(true, _module_under_test.push(ev));\n    }\n    auto vst_events = _module_under_test.flush();\n    ASSERT_EQ(TEST_DATA_SIZE, vst_events->numEvents);\n    for (int i=0; i<TEST_DATA_SIZE; i++)\n    {\n        auto midi_ev = reinterpret_cast<VstMidiEvent*>(vst_events->events[i]);\n        EXPECT_EQ(i, midi_ev->deltaFrames);\n    }\n}\n\nTEST_F(TestVst2xMidiEventFIFO, TestNoteOnCreation)\n{\n    _module_under_test.flush();\n    auto ev = RtEvent::make_note_on_event(0, 0, 0, 60, 1.0f);\n    _module_under_test.push(ev);\n    auto vst_events = _module_under_test.flush();\n    auto midi_ev = reinterpret_cast<VstMidiEvent*>(vst_events->events[0]);\n\n    EXPECT_EQ(static_cast<char>(144), midi_ev->midiData[0]);\n    EXPECT_EQ(static_cast<char>(60), midi_ev->midiData[1]);\n    EXPECT_EQ(static_cast<char>(127), midi_ev->midiData[2]);\n}\n\nTEST_F(TestVst2xMidiEventFIFO, TestNoteOffCreation)\n{\n    _module_under_test.flush();\n    auto ev = RtEvent::make_note_off_event(0, 0, 0, 72, 0.5f);\n    _module_under_test.push(ev);\n    auto vst_events = _module_under_test.flush();\n    auto midi_ev = reinterpret_cast<VstMidiEvent*>(vst_events->events[0]);\n\n    EXPECT_EQ(static_cast<char>(128), midi_ev->midiData[0]);\n    EXPECT_EQ(static_cast<char>(72), midi_ev->midiData[1]);\n    EXPECT_EQ(static_cast<char>(64), midi_ev->midiData[2]);\n}\n\nTEST_F(TestVst2xMidiEventFIFO, TestNoteAftertouchCreation)\n{\n    _module_under_test.flush();\n    auto ev = RtEvent::make_note_aftertouch_event(0, 0, 0, 127, 0.0f);\n    _module_under_test.push(ev);\n    auto vst_events = _module_under_test.flush();\n    auto midi_ev = reinterpret_cast<VstMidiEvent*>(vst_events->events[0]);\n\n    EXPECT_EQ(static_cast<char>(160), midi_ev->midiData[0]);\n    EXPECT_EQ(static_cast<char>(127), midi_ev->midiData[1]);\n    EXPECT_EQ(static_cast<char>(0), midi_ev->midiData[2]);\n}\n\nTEST_F(TestVst2xMidiEventFIFO, TestWrappedMidiCreation)\n{\n    _module_under_test.flush();\n    auto ev = RtEvent::make_wrapped_midi_event(0, 0, {176u, 21u, 64u, 0u});\n    _module_under_test.push(ev);\n    auto vst_events = _module_under_test.flush();\n    auto midi_ev = reinterpret_cast<VstMidiEvent*>(vst_events->events[0]);\n\n    EXPECT_EQ(static_cast<char>(176), midi_ev->midiData[0]);\n    EXPECT_EQ(static_cast<char>(21), midi_ev->midiData[1]);\n    EXPECT_EQ(static_cast<char>(64), midi_ev->midiData[2]);\n}\n\n\n\n"
  },
  {
    "path": "test/unittests/library/vst2x_plugin_loading_test.cpp",
    "content": "#include <filesystem>\n\n#include \"gtest/gtest.h\"\n\n#include \"test_utils/host_control_mockup.h\"\n#include \"library/plugin_registry.h\"\n\n#include \"library/vst2x/vst2x_plugin_loader.cpp\"\n\nconstexpr float SAMPLE_RATE = 44000;\n\nusing namespace sushi;\n\n// Empty fixture as PluginLoader has only static methods so far\n\nclass TestVst2xPluginLoading : public ::testing::Test\n{\nprotected:\n    TestVst2xPluginLoading():\n            _host_control(_hc.make_host_control_mockup(SAMPLE_RATE))\n    {}\n\n    HostControlMockup _hc;\n    HostControl _host_control;\n    PluginRegistry _plugin_registry;\n};\n\nTEST_F(TestVst2xPluginLoading, TestPluginRegistryVst2xLoading)\n{\n    auto full_path = std::filesystem::path(VST2_TEST_PLUGIN_PATH);\n    auto full_again_path = std::string(std::filesystem::absolute(full_path).string());\n\n    PluginInfo plugin_info;\n    plugin_info.uid = \"\";\n    // dlopen on Linux requires absolute paths if library is not on system paths already\n    plugin_info.path = full_again_path;\n    plugin_info.type = PluginType::VST2X;\n\n    auto [processor_status, processor] = _plugin_registry.new_instance(plugin_info,\n                                                                       _host_control,\n                                                                       SAMPLE_RATE);\n\n    ASSERT_EQ(processor_status, ProcessorReturnCode::OK);\n}\n\nTEST_F(TestVst2xPluginLoading, TestLoadPlugin)\n{\n    // dlopen on Linux requires absolute paths if library is not on system paths already\n    auto full_path = std::filesystem::path(VST2_TEST_PLUGIN_PATH);\n    auto full_again_path = std::string(std::filesystem::absolute(full_path).string());\n\n    auto library_handle = vst2::PluginLoader::get_library_handle_for_plugin(full_again_path);\n    ASSERT_NE(nullptr, library_handle);\n    auto plugin = vst2::PluginLoader::load_plugin(library_handle);\n\n    // Check magic number\n    EXPECT_EQ(kEffectMagic, plugin->magic);\n\n    vst2::PluginLoader::close_library_handle(library_handle);\n}\n"
  },
  {
    "path": "test/unittests/library/vst2x_wrapper_test.cpp",
    "content": "#include <filesystem>\n\n#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/host_control_mockup.h\"\n\n#include \"library/vst2x/vst2x_wrapper.cpp\"\n\nnamespace sushi::internal::vst2\n{\n\nclass Vst2xWrapperAccessor\n{\npublic:\n    explicit Vst2xWrapperAccessor(Vst2xWrapper& f) : _friend(f) {}\n\n    [[nodiscard]] bool can_do_soft_bypass() const\n    {\n        return _friend._can_do_soft_bypass;\n    }\n\n    AEffect* plugin_handle()\n    {\n        return _friend._plugin_handle;\n    }\n\n    [[nodiscard]] float sample_rate() const\n    {\n        return _friend._sample_rate;\n    }\n\n    void notify_parameter_change(VstInt32 parameter_index, float value)\n    {\n        _friend.notify_parameter_change(parameter_index, value);\n    }\n\n    void notify_parameter_change_rt(VstInt32 parameter_index, float value)\n    {\n        _friend.notify_parameter_change_rt(parameter_index, value);\n    }\n\nprivate:\n    Vst2xWrapper& _friend;\n};\n\n}\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::vst2;\n\n// Reference output signal from TestPlugin\n// in response to NoteON C4 (60), vel=127, default parameters\nstatic constexpr float TEST_SYNTH_EXPECTED_OUT[2][64] = {\n        {\n                1, 0.999853f, 0.999414f, 0.998681f, 0.997655f, 0.996337f, 0.994727f, 0.992825f,\n                0.990632f, 0.988149f, 0.985375f, 0.982313f, 0.978963f, 0.975326f, 0.971403f,\n                0.967195f, 0.962703f, 0.95793f, 0.952875f, 0.947541f, 0.941929f, 0.936041f,\n                0.929879f, 0.923443f, 0.916738f, 0.909763f, 0.902521f, 0.895015f, 0.887247f,\n                0.879218f, 0.870932f, 0.86239f, 0.853596f, 0.844551f, 0.835258f, 0.825721f,\n                0.815941f, 0.805923f, 0.795668f, 0.785179f, 0.774461f, 0.763515f, 0.752346f,\n                0.740956f, 0.729348f, 0.717527f, 0.705496f, 0.693257f, 0.680815f, 0.668174f,\n                0.655337f, 0.642307f, 0.62909f, 0.615688f, 0.602105f, 0.588346f, 0.574414f,\n                0.560314f, 0.546049f, 0.531625f, 0.517045f, 0.502313f, 0.487433f, 0.472411f\n        },\n        {\n                0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f,\n                0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f,\n                0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f,\n                0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f,\n                0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f,\n                0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f,\n                0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f,\n                0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f,\n                0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f,\n                0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f,\n                0.5f, 0.5f, 0.5f, 0.5f\n        }\n};\n\nconstexpr float TEST_SAMPLE_RATE = 48000;\nconstexpr int   TEST_CHANNEL_COUNT = 2;\n\nclass TestVst2xWrapper : public ::testing::Test\n{\nprotected:\n    using ::testing::Test::SetUp; // Hide error of hidden overload of virtual function in clang when signatures differ but the name is the same\n    TestVst2xWrapper() = default;\n\n    void SetUp(const std::string& plugin_path)\n    {\n        auto full_path = std::filesystem::path(plugin_path.c_str());\n        auto full_plugin_path = std::string(std::filesystem::absolute(full_path).string());\n\n        _module_under_test = std::make_unique<Vst2xWrapper>(_host_control.make_host_control_mockup(TEST_SAMPLE_RATE), full_plugin_path);\n\n        _accessor = std::make_unique<Vst2xWrapperAccessor>(*_module_under_test);\n\n        auto ret = _module_under_test->init(TEST_SAMPLE_RATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, ret);\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n        _module_under_test->set_event_output(&_host_control._event_output);\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<Vst2xWrapper> _module_under_test;\n\n    std::unique_ptr<Vst2xWrapperAccessor> _accessor;\n};\n\n\nTEST_F(TestVst2xWrapper, TestSetName)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    EXPECT_EQ(\"Test Plugin\", _module_under_test->name());\n    EXPECT_EQ(\"Test Plugin\", _module_under_test->label());\n}\n\nTEST_F(TestVst2xWrapper, TestSetChannels)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    EXPECT_EQ(2, _module_under_test->input_channels());\n    _module_under_test->set_channels(1, 1);\n\n    EXPECT_EQ(1, _module_under_test->input_channels());\n    EXPECT_EQ(1, _module_under_test->output_channels());\n}\n\nTEST_F(TestVst2xWrapper, TestParameterInitialization)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    auto gain_param = _module_under_test->parameter_from_name(\"Gain\");\n    EXPECT_TRUE(gain_param);\n    EXPECT_EQ(0u, gain_param->id());\n    EXPECT_EQ(\"Gain\", gain_param->name());\n    EXPECT_EQ(\"Gain\", gain_param->label());\n    EXPECT_EQ(\"dB\", gain_param->unit());\n}\n\nTEST_F(TestVst2xWrapper, TestPluginCanDos)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    EXPECT_FALSE(_accessor->can_do_soft_bypass());\n}\n\nTEST_F(TestVst2xWrapper, TestParameterSetViaEvent)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    auto event = RtEvent::make_parameter_change_event(0, 0, 0, 0.123f);\n    _module_under_test->process_event(event);\n    auto handle = _accessor->plugin_handle();\n    EXPECT_EQ(0.123f, handle->getParameter(handle, 0));\n}\n\nTEST_F(TestVst2xWrapper, TestProcess)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    ChunkSampleBuffer in_buffer(2);\n    ChunkSampleBuffer out_buffer(2);\n\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(1.0f, out_buffer);\n}\n\nTEST_F(TestVst2xWrapper, TestMonoProcess)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    ChunkSampleBuffer mono_buffer(1);\n    ChunkSampleBuffer stereo_buffer(2);\n\n    _module_under_test->set_channels(1, 2);\n    test_utils::fill_sample_buffer(mono_buffer, 1.0f);\n    _module_under_test->process_audio(mono_buffer, stereo_buffer);\n\n    auto left = ChunkSampleBuffer::create_non_owning_buffer(stereo_buffer, 0, 1);\n    auto right = ChunkSampleBuffer::create_non_owning_buffer(stereo_buffer, 1, 1);\n    test_utils::assert_buffer_value(1.0f, left);\n    test_utils::assert_buffer_value(0.0f, right);\n\n    _module_under_test->set_channels(2, 1);\n    test_utils::fill_sample_buffer(stereo_buffer, 2.0f);\n    _module_under_test->process_audio(stereo_buffer, mono_buffer);\n    test_utils::assert_buffer_value(2.0f, mono_buffer);\n}\n\nTEST_F(TestVst2xWrapper, TestProcessingWithParameterChanges)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    ChunkSampleBuffer in_buffer(2);\n    ChunkSampleBuffer out_buffer(2);\n    auto event = RtEvent::make_parameter_change_event(0, 0, 0, 0.123f);\n\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(1.0f, out_buffer);\n\n    // Verify that a parameter change affects the sound\n    _module_under_test->process_event(event);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(0.123f, out_buffer);\n\n    // Verify that we can retrive the new value\n    auto [status, value] = _module_under_test->parameter_value(0);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    ASSERT_FLOAT_EQ(0.123f, value);\n}\n\nTEST_F(TestVst2xWrapper, TestBypassProcessing)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    ChunkSampleBuffer in_buffer(2);\n    ChunkSampleBuffer out_buffer(2);\n    // Set the gain to 0.5\n    auto event = RtEvent::make_parameter_change_event(0, 0, 0, 0.5f);\n    _module_under_test->process_event(event);\n\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n\n    // Set bypass and manually feed the generated RtEvent back to the\n    // wrapper processor as event dispatcher is not running\n    _module_under_test->set_bypassed(true);\n    auto bypass_event = _host_control._dummy_dispatcher.retrieve_event();\n    EXPECT_TRUE(bypass_event.get());\n    _module_under_test->process_event(bypass_event->to_rt_event(0));\n    EXPECT_TRUE(_module_under_test->bypassed());\n    _module_under_test->process_audio(in_buffer, out_buffer);\n\n    // Test that we are ramping up the audio to the bypass value\n    float prev_value = 0;\n    for (int i = 1; i< AUDIO_CHUNK_SIZE; ++i)\n    {\n        EXPECT_GT(out_buffer.channel(0)[i], prev_value);\n        prev_value = out_buffer.channel(0)[i];\n    }\n}\n\nTEST_F(TestVst2xWrapper, TestTimeInfo)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    _host_control._transport.set_playing_mode(PlayingMode::PLAYING, false);\n    _host_control._transport.set_tempo(60, false);\n    _host_control._transport.set_time_signature({4, 4}, false);\n    _host_control._transport.set_time(Time(0), 0);\n    _host_control._transport.set_time(std::chrono::seconds(2), static_cast<int64_t>(TEST_SAMPLE_RATE) * 2);\n    auto time_info = _module_under_test->time_info();\n    /* For these numbers to match exactly, we need to choose a time interval which\n     * is an integer multiple of AUDIO_CHUNK_SIZE, hence 2 seconds at 48000, which\n     * is good up to AUDIO_CHUNK_SIZE = 256 */\n    EXPECT_EQ(static_cast<int64_t>(TEST_SAMPLE_RATE) * 2, time_info->samplePos);\n    EXPECT_EQ(2'000'000'000, time_info->nanoSeconds);\n    EXPECT_FLOAT_EQ(2.0f, static_cast<float>(time_info->ppqPos));\n    EXPECT_FLOAT_EQ(60.0f, static_cast<float>(time_info->tempo));\n    EXPECT_FLOAT_EQ(0.0f, static_cast<float>(time_info->barStartPos));\n    EXPECT_EQ(4, time_info->timeSigNumerator);\n    EXPECT_EQ(4, time_info->timeSigDenominator);\n}\n\nTEST_F(TestVst2xWrapper, TestMidiEvents)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    ChunkSampleBuffer in_buffer(2);\n    ChunkSampleBuffer out_buffer(2);\n\n    _module_under_test->process_event(RtEvent::make_note_on_event(0, 0, 0, 60, 1.0f));\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    for (int i=0; i<2; i++)\n    {\n        for (int j=0; j < std::min(AUDIO_CHUNK_SIZE, 64); j++)\n        {\n            ASSERT_NEAR(TEST_SYNTH_EXPECTED_OUT[i][j], out_buffer.channel(i)[j], 0.00001);\n        }\n    }\n\n    // Send NoteOFF, plugin should immediately silence everything\n    _module_under_test->process_event(RtEvent::make_note_off_event(0, 0, 0, 60, 1.0f));\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(0.0f, out_buffer);\n}\n\nTEST_F(TestVst2xWrapper, TestConfigurationChange)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    _module_under_test->configure(44100.0f);\n    ASSERT_FLOAT_EQ(44100, _accessor->sample_rate());\n}\n\nTEST_F(TestVst2xWrapper, TestParameterChangeNotifications)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    EXPECT_FALSE(_host_control._dummy_dispatcher.got_event());\n    _accessor->notify_parameter_change(0, 0.5f);\n    auto event = _host_control._dummy_dispatcher.retrieve_event();\n    ASSERT_FALSE(event == nullptr);\n    ASSERT_TRUE(event->is_parameter_change_notification());\n}\n\nTEST_F(TestVst2xWrapper, TestRTParameterChangeNotifications)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    RtSafeRtEventFifo queue;\n    _module_under_test->set_event_output(&queue);\n    ASSERT_TRUE(queue.empty());\n    _accessor->notify_parameter_change_rt(0, 0.5f);\n    RtEvent event;\n    auto received = queue.pop(event);\n    ASSERT_TRUE(received);\n    ASSERT_EQ(RtEventType::FLOAT_PARAMETER_CHANGE, event.type());\n}\n\nTEST_F(TestVst2xWrapper, TestProgramManagement)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n    ASSERT_TRUE(_module_under_test->supports_programs());\n    ASSERT_EQ(3, _module_under_test->program_count());\n    ASSERT_EQ(0, _module_under_test->current_program());\n    _module_under_test->set_program(1);\n    ASSERT_EQ(1, _module_under_test->current_program());\n    ASSERT_EQ(\"Program 2\", _module_under_test->current_program_name());\n    auto [status, program_name] = _module_under_test->program_name(2);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    ASSERT_EQ(\"Program 3\", program_name);\n    // Access with an invalid program number\n    std::tie(status, program_name) = _module_under_test->program_name(2000);\n    ASSERT_NE(ProcessorReturnCode::OK, status);\n    // Get all programs,\n    auto [res, programs] = _module_under_test->all_program_names();\n    ASSERT_EQ(ProcessorReturnCode::OK, res);\n    ASSERT_EQ(\"Program 2\", programs[1]);\n    ASSERT_EQ(3u, programs.size());\n}\n\nTEST_F(TestVst2xWrapper, TestStateHandling)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n\n    ProcessorState state;\n    state.set_bypass(true);\n    state.set_program(2);\n    state.add_parameter_change(1, 0.33f);\n\n    auto status = _module_under_test->set_state(&state, false);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    // Check that new values are set\n    EXPECT_FLOAT_EQ(0.33f, _module_under_test->parameter_value(1).second);\n    EXPECT_TRUE(_module_under_test->bypassed());\n    EXPECT_EQ(2, _module_under_test->current_program());\n    EXPECT_EQ(\"Program 3\", _module_under_test->current_program_name());\n\n    // Test with realtime set to true\n    state.set_bypass(false);\n    state.set_program(1);\n    state.add_parameter_change(1, 0.5f);\n\n    status = _module_under_test->set_state(&state, true);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    while (auto event = _host_control._dummy_dispatcher.retrieve_event())\n    {\n        _module_under_test->process_event(event->to_rt_event(0));\n    }\n\n    // Check that new values are set\n    EXPECT_FLOAT_EQ(0.5f, _module_under_test->parameter_value(1).second);\n    EXPECT_FALSE(_module_under_test->bypassed());\n    EXPECT_EQ(1, _module_under_test->current_program());\n    EXPECT_EQ(\"Program 2\", _module_under_test->current_program_name());\n\n    // Retrieve the delete event and execute it\n    ASSERT_FALSE(_host_control._event_output.empty());\n    auto rt_event = _host_control._event_output.pop();\n    auto delete_event = Event::from_rt_event(rt_event, IMMEDIATE_PROCESS);\n    ASSERT_TRUE(delete_event);\n    static_cast<AsynchronousDeleteEvent*>(delete_event.get())->execute();\n}\n\nTEST_F(TestVst2xWrapper, TestStateSaving)\n{\n    SetUp(VST2_TEST_PLUGIN_PATH);\n\n    float parameter_value = _module_under_test->parameter_value(1).second;\n\n    auto state = _module_under_test->save_state();\n    EXPECT_FALSE(state.has_binary_data());\n    ASSERT_FALSE(state.parameters().empty());\n    EXPECT_FLOAT_EQ(parameter_value, state.parameters()[1].second);\n    EXPECT_TRUE(state.bypassed().has_value());\n}\n\n"
  },
  {
    "path": "test/unittests/library/vst3x_wrapper_test.cpp",
    "content": "#include <filesystem>\n\n#include \"gtest/gtest.h\"\n\n#define TWINE_EXPOSE_INTERNALS 1\n#include <twine/twine.h>\n\n#include \"test_utils/test_utils.h\"\n#include \"library/rt_event_fifo.h\"\n#include \"library/vst3x/vst3x_utils.cpp\"\n#include \"test_utils/host_control_mockup.h\"\n\n#include \"test_utils/vst3_test_plugin.h\"\n#include \"library/vst3x/vst3x_wrapper.cpp\"\n#include \"library/vst3x/vst3x_host_app.cpp\"\n#include \"library/vst3x/vst3x_file_utils.cpp\"\n\nnamespace sushi::internal::vst3\n{\n\nclass Vst3xWrapperAccessor\n{\npublic:\n    explicit Vst3xWrapperAccessor(Vst3xWrapper& f) : _friend(f) {}\n\n    bool inject_plugin(Steinberg::Vst::IComponent* component, const std::string& name, float sample_rate)\n    {\n        _friend._sample_rate = sample_rate;\n        bool loaded = _friend._instance.load_plugin_from_component(component,\n                                                                   name,\n                                                                   &_friend._component_handler);\n        if (!loaded)\n        {\n            _friend._cleanup();\n            return false;\n        }\n        _friend.set_name(_friend._instance.name());\n        _friend.set_label(_friend._instance.name());\n\n        auto res = _friend._setup();\n        if (res != ProcessorReturnCode::OK)\n        {\n            return false;\n        }\n        return true;\n    }\n\n    Vst3xWrapper::SpecialParameter& bypass_parameter()\n    {\n        return _friend._bypass_parameter;\n    }\n\n    SushiProcessData& process_data()\n    {\n        return _friend._process_data;\n    }\n\n    PluginInstance& instance()\n    {\n        return _friend._instance;\n    }\n\n    void forward_events(Steinberg::Vst::ProcessData& data)\n    {\n        _friend._forward_events(data);\n    }\n\n    float sample_rate()\n    {\n        return _friend._sample_rate;\n    }\n\n    void fill_processing_context()\n    {\n        return _friend._fill_processing_context();\n    }\n\n    void forward_params(Steinberg::Vst::ProcessData& data)\n    {\n        _friend. _forward_params(data);\n    }\n\n    memory_relaxed_aquire_release::CircularFifo<Vst3xRtState*, STATE_CHANGE_QUEUE_SIZE>& state_change_queue()\n    {\n        return _friend._state_change_queue;\n    }\n\nprivate:\n    Vst3xWrapper& _friend;\n};\n\n}\n\nusing namespace sushi;\nusing namespace sushi::internal::vst3;\n\nconst char PLUGIN_NAME[] = \"ADelay\";\n\n#ifdef _MSC_VER\nauto UNITTEST_EXE = \"unit_tests.exe\";\n#else\nauto UNITTEST_EXE = \"unit_tests\";\n#endif\n\nconstexpr unsigned int DELAY_PARAM_ID = 100;\nconstexpr unsigned int BYPASS_PARAM_ID = 101;\nconstexpr float TEST_SAMPLE_RATE = 48000;\nconstexpr int   TEST_CHANNEL_COUNT = 2;\n\n/* Quick test to test plugin loading */\nTEST(TestVst3xPluginInstance, TestLoadPlugin)\n{\n    auto full_path = std::filesystem::path(SUSHI_VST3_TEST_PLUGIN_PATH);\n    auto full_test_plugin_path = std::filesystem::absolute(full_path).string();\n\n    SushiHostApplication host_app(nullptr);\n    PluginInstance module_under_test(&host_app);\n    bool success = module_under_test.load_plugin(full_test_plugin_path, PLUGIN_NAME, nullptr);\n    ASSERT_TRUE(success);\n    ASSERT_TRUE(module_under_test.processor());\n    ASSERT_TRUE(module_under_test.component());\n    ASSERT_TRUE(module_under_test.controller());\n}\n\n/* Test that nothing breaks if the plugin is not found */\nTEST(TestVst3xPluginInstance, TestLoadPluginFromErroneousFilename)\n{\n    /* Non-existing library */\n    SushiHostApplication host_app(nullptr);\n    PluginInstance module_under_test(&host_app);\n    bool success = module_under_test.load_plugin(\"/usr/lib/lxvst/no_plugin.vst3\", PLUGIN_NAME, nullptr);\n    ASSERT_FALSE(success);\n\n    /* Existing library but non-existing plugin */\n    auto full_path = std::filesystem::path(SUSHI_VST3_TEST_PLUGIN_PATH);\n    auto full_test_plugin_path = std::filesystem::absolute(full_path).string();\n\n    success = module_under_test.load_plugin(full_test_plugin_path, \"NoPluginWithThisName\", nullptr);\n    ASSERT_FALSE(success);\n}\n\nTEST(TestVst3xRtState, TestOperation)\n{\n    ProcessorState state;\n    state.add_parameter_change(3, 0.5f);\n    state.add_parameter_change(10, 0.25f);\n    Vst3xRtState module_under_test(state);\n\n    EXPECT_EQ(2, module_under_test.getParameterCount());\n    auto data = module_under_test.getParameterData(0);\n    ASSERT_TRUE(data);\n    EXPECT_EQ(1, data->getPointCount());\n    EXPECT_EQ(3, data->getParameterId());\n    Steinberg::Vst::ParamValue value = 0;\n    Steinberg::int32 offset = -1;\n\n    EXPECT_EQ(Steinberg::kResultOk, data->getPoint(0, offset, value));\n    EXPECT_FLOAT_EQ(0.5, value);\n    EXPECT_EQ(0, offset);\n\n    data = module_under_test.getParameterData(1);\n    ASSERT_TRUE(data);\n    EXPECT_EQ(10, data->getParameterId());\n    EXPECT_EQ(Steinberg::kResultOk, data->getPoint(0, offset, value));\n    EXPECT_FLOAT_EQ(0.25, value);\n    EXPECT_EQ(0, offset);\n\n    data = module_under_test.getParameterData(2);\n    EXPECT_FALSE(data);\n}\n\n\nclass TestVst3xWrapper : public ::testing::Test\n{\nprotected:\n    using ::testing::Test::SetUp; // Hide error of hidden overload of virtual function in clang when signatures differ but the name is the same\n    TestVst3xWrapper() = default;\n\n    // Setup function to load an external plugin from file\n    void SetUp(const char* plugin_file, const char* plugin_name)\n    {\n        auto full_path = std::filesystem::path(plugin_file);\n        auto full_plugin_path = std::string(std::filesystem::absolute(full_path).string());\n\n        _module_under_test = std::make_unique<Vst3xWrapper>(_host_control.make_host_control_mockup(TEST_SAMPLE_RATE),\n                                                            full_plugin_path,\n                                                            plugin_name);\n\n        _accessor = std::make_unique<Vst3xWrapperAccessor>(*_module_under_test);\n\n        auto ret = _module_under_test->init(TEST_SAMPLE_RATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, ret);\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_event_output(&_event_queue);\n        _module_under_test->set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n    }\n\n    // Setup function to load a plugin from an existing class\n    void SetUp(test_utils::Vst3TestPlugin* plugin)\n    {\n        _module_under_test = std::make_unique<Vst3xWrapper>(_host_control.make_host_control_mockup(TEST_SAMPLE_RATE),\n                                                            \"\",\n                                                            \"test_plugin\");\n\n        _accessor = std::make_unique<Vst3xWrapperAccessor>(*_module_under_test);\n\n        ASSERT_TRUE(_accessor->inject_plugin(plugin, \"test_plugin\", TEST_SAMPLE_RATE));\n\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_event_output(&_event_queue);\n        _module_under_test->set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<Vst3xWrapper> _module_under_test;\n\n    std::unique_ptr<Vst3xWrapperAccessor> _accessor;\n\n    std::unique_ptr<test_utils::Vst3TestPlugin> _test_plugin;\n\n    RtSafeRtEventFifo _event_queue;\n};\n\nTEST_F(TestVst3xWrapper, TestLoadAndInitPlugin)\n{\n    SetUp(SUSHI_VST3_TEST_PLUGIN_PATH, PLUGIN_NAME);\n    ASSERT_TRUE(_module_under_test);\n    EXPECT_EQ(\"ADelay\", _module_under_test->name());\n\n    auto parameters = _module_under_test->all_parameters();\n    ASSERT_EQ(1u, parameters.size());\n    EXPECT_EQ(\"Delay\", parameters[0]->name());\n    EXPECT_EQ(DELAY_PARAM_ID, parameters[0]->id());\n    EXPECT_TRUE(_accessor->bypass_parameter().supported);\n    EXPECT_EQ(BYPASS_PARAM_ID, static_cast<unsigned int>(_accessor->bypass_parameter().id));\n\n    auto descriptor = _module_under_test->parameter_from_name(\"Delay\");\n    ASSERT_TRUE(descriptor);\n    EXPECT_EQ(DELAY_PARAM_ID, descriptor->id());\n\n    descriptor = _module_under_test->parameter_from_id(DELAY_PARAM_ID);\n    ASSERT_TRUE(descriptor);\n    EXPECT_EQ(DELAY_PARAM_ID, descriptor->id());\n\n    descriptor = _module_under_test->parameter_from_id(12345);\n    ASSERT_FALSE(descriptor);\n}\n\nTEST_F(TestVst3xWrapper, TestProcessing)\n{\n    SetUp(SUSHI_VST3_TEST_PLUGIN_PATH, PLUGIN_NAME);\n    ChunkSampleBuffer in_buffer(2);\n    ChunkSampleBuffer out_buffer(2);\n    test_utils::fill_sample_buffer(in_buffer, 1);\n    /* Set delay to 0 */\n    auto event = RtEvent::make_parameter_change_event(0u, 0, DELAY_PARAM_ID, 0.0f);\n\n    _module_under_test->set_enabled(true);\n    _module_under_test->process_event(event);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n\n    /* Minimum delay will still be 1 sample */\n    EXPECT_FLOAT_EQ(0.0f, out_buffer.channel(0)[0]);\n    EXPECT_FLOAT_EQ(0.0f, out_buffer.channel(1)[0]);\n    EXPECT_FLOAT_EQ(1.0f, out_buffer.channel(0)[1]);\n    EXPECT_FLOAT_EQ(1.0f, out_buffer.channel(1)[1]);\n}\n\nTEST_F(TestVst3xWrapper, TestBypassProcessing)\n{\n    SetUp(SUSHI_VST3_TEST_PLUGIN_PATH, PLUGIN_NAME);\n    ChunkSampleBuffer in_buffer(2);\n    ChunkSampleBuffer out_buffer(2);\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n    // The adelay example supports soft bypass.\n    EXPECT_TRUE(_accessor->bypass_parameter().supported);\n    EXPECT_EQ(BYPASS_PARAM_ID, _accessor->bypass_parameter().id);\n\n    // Set bypass and manually feed the generated RtEvent back to the\n    // wrapper processor as event dispatcher is not running\n    _module_under_test->set_bypassed(true);\n    auto bypass_event = _host_control._dummy_dispatcher.retrieve_event();\n    EXPECT_TRUE(bypass_event.get());\n    _module_under_test->process_event(bypass_event->to_rt_event(0));\n    _module_under_test->process_audio(in_buffer, out_buffer);\n\n    // Manually call the event callback to send the update back to the\n    // controller, as eventloop is not running\n    _module_under_test->parameter_update_callback(_module_under_test.get(), 0);\n    EXPECT_TRUE(_module_under_test->bypassed());\n\n    // Don't test actual bypass processing because the ADelay example\n    // doesn't implement that...\n}\n\nTEST_F(TestVst3xWrapper, TestEventForwarding)\n{\n    SetUp(SUSHI_VST3_TEST_PLUGIN_PATH, PLUGIN_NAME);\n\n    Steinberg::Vst::Event note_on_event;\n    note_on_event.type = Steinberg::Vst::Event::EventTypes::kNoteOnEvent;\n    note_on_event.sampleOffset = 5;\n    note_on_event.noteOn.velocity = 1.0f;\n    note_on_event.noteOn.channel = 1;\n    note_on_event.noteOn.pitch = 46;\n\n    Steinberg::Vst::Event note_off_event;\n    note_off_event.type = Steinberg::Vst::Event::EventTypes::kNoteOffEvent;\n    note_off_event.sampleOffset = 6;\n    note_off_event.noteOff.velocity = 1.0f;\n    note_off_event.noteOff.channel = 2;\n    note_off_event.noteOff.pitch = 48;\n\n    _accessor->process_data().outputEvents->addEvent(note_on_event);\n    _accessor->process_data().outputEvents->addEvent(note_off_event);\n    _accessor->forward_events(_accessor->process_data());\n\n    ASSERT_FALSE(_event_queue.empty());\n    RtEvent event;\n    ASSERT_TRUE(_event_queue.pop(event));\n    ASSERT_EQ(RtEventType::NOTE_ON, event.type());\n    ASSERT_EQ(5, event.sample_offset());\n    ASSERT_EQ(46, event.keyboard_event()->note());\n    ASSERT_FLOAT_EQ(1.0f, event.keyboard_event()->velocity());\n\n    ASSERT_TRUE(_event_queue.pop(event));\n    ASSERT_EQ(RtEventType::NOTE_OFF, event.type());\n    ASSERT_EQ(6, event.sample_offset());\n    ASSERT_EQ(48, event.keyboard_event()->note());\n    ASSERT_FLOAT_EQ(1.0f, event.keyboard_event()->velocity());\n\n    ASSERT_FALSE(_event_queue.pop(event));\n}\n\nTEST_F(TestVst3xWrapper, TestConfigurationChange)\n{\n    SetUp(SUSHI_VST3_TEST_PLUGIN_PATH, PLUGIN_NAME);\n    _module_under_test->configure(44100.0f);\n    ASSERT_FLOAT_EQ(44100, _accessor->sample_rate());\n}\n\nTEST_F(TestVst3xWrapper, TestTimeInfo)\n{\n    SetUp(SUSHI_VST3_TEST_PLUGIN_PATH, PLUGIN_NAME);\n    _host_control._transport.set_playing_mode(PlayingMode::PLAYING, false);\n    _host_control._transport.set_tempo(120, false);\n    _host_control._transport.set_time_signature({3, 4}, false);\n    _host_control._transport.set_time(Time(0), 0);\n    _host_control._transport.set_time(std::chrono::seconds(2), static_cast<int64_t>(TEST_SAMPLE_RATE) * 2);\n\n    _accessor->fill_processing_context();\n    auto context = _accessor->process_data().processContext;\n    /* For these numbers to match exactly, we need to choose a time interval which\n     * is an integer multiple of AUDIO_CHUNK_SIZE, hence 2 seconds at 48000, which\n     * is good up to AUDIO_CHUNK_SIZE = 256 */\n    EXPECT_FLOAT_EQ(TEST_SAMPLE_RATE, context->sampleRate);\n    EXPECT_EQ(static_cast<int64_t>(TEST_SAMPLE_RATE) * 2, context->projectTimeSamples);\n    EXPECT_EQ(2'000'000'000, context->systemTime);\n    EXPECT_EQ(static_cast<int64_t>(TEST_SAMPLE_RATE) * 2, context->continousTimeSamples);\n    EXPECT_FLOAT_EQ(4.0f, context->projectTimeMusic);\n    EXPECT_FLOAT_EQ(3.0f, context->barPositionMusic);\n    EXPECT_FLOAT_EQ(120.0f, context->tempo);\n    EXPECT_EQ(3, context->timeSigNumerator);\n    EXPECT_EQ(4, context->timeSigDenominator);\n}\n\nTEST_F(TestVst3xWrapper, TestParameterHandling)\n{\n    ChunkSampleBuffer in_buffer(2);\n    ChunkSampleBuffer out_buffer(2);\n    SetUp(SUSHI_VST3_TEST_PLUGIN_PATH, PLUGIN_NAME);\n\n    auto [status, value] = _module_under_test->parameter_value(DELAY_PARAM_ID);\n    EXPECT_EQ(ProcessorReturnCode::OK, status);\n    EXPECT_FLOAT_EQ(1.0f, value);\n\n    auto event = RtEvent::make_parameter_change_event(_module_under_test->id(), 0, DELAY_PARAM_ID, 0.5f);\n    _module_under_test->process_event(event);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    // Manually call the event callback to send the update back to the controller, as eventloop is not running\n    _module_under_test->parameter_update_callback(_module_under_test.get(), 0);\n\n    std::tie(status, value) = _module_under_test->parameter_value(DELAY_PARAM_ID);\n    EXPECT_EQ(ProcessorReturnCode::OK, status);\n    EXPECT_FLOAT_EQ(0.5f, value);\n\n    std::string string_repr;\n    std::tie(status, string_repr) = _module_under_test->parameter_value_formatted(DELAY_PARAM_ID);\n    EXPECT_EQ(ProcessorReturnCode::OK, status);\n    EXPECT_EQ(\"0.5000\", string_repr);\n}\n\nTEST_F(TestVst3xWrapper, TestOutputParameterHandling)\n{\n    ChunkSampleBuffer in_buffer(2);\n    ChunkSampleBuffer out_buffer(2);\n\n    Steinberg::IPtr<test_utils::Vst3TestPlugin> plugin = new test_utils::Vst3TestPlugin();\n    SetUp(plugin.get());\n\n    _module_under_test->set_enabled(true);\n\n    auto parameters = _module_under_test->all_parameters();\n    // 2 normal float parameters and 2 string properties\n    ASSERT_EQ(4u, parameters.size());\n    EXPECT_EQ(\"Delay\", parameters[0]->name());\n    EXPECT_EQ(\"sec\", parameters[0]->unit());\n    EXPECT_EQ(2, parameters[0]->id());\n    EXPECT_TRUE(parameters[0]->automatable());\n\n\n    EXPECT_EQ(\"Output\", parameters[1]->name());\n    EXPECT_EQ(\"\", parameters[1]->unit());\n    EXPECT_EQ(3, parameters[1]->id());\n    EXPECT_FALSE(parameters[1]->automatable());\n}\n\nTEST_F(TestVst3xWrapper, TestGateOutput)\n{\n    SetUp(SUSHI_VST3_TEST_PLUGIN_PATH, PLUGIN_NAME);\n\n    auto status = _module_under_test->connect_gate_from_processor(2, 0, 46);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    Steinberg::Vst::Event note_on_event;\n    note_on_event.type = Steinberg::Vst::Event::EventTypes::kNoteOnEvent;\n    note_on_event.sampleOffset = 5;\n    note_on_event.noteOn.velocity = 1.0f;\n    note_on_event.noteOn.channel = 0;\n    note_on_event.noteOn.pitch = 46;\n\n    _accessor->process_data().outputEvents->addEvent(note_on_event);\n    _accessor->forward_events(_accessor->process_data());\n\n    ASSERT_FALSE(_event_queue.empty());\n    RtEvent event;\n    ASSERT_TRUE(_event_queue.pop(event));\n    ASSERT_EQ(RtEventType::GATE_EVENT, event.type());\n    ASSERT_EQ(0, event.sample_offset());\n    ASSERT_EQ(2, event.gate_event()->gate_no());\n    ASSERT_TRUE(event.gate_event()->value());\n\n    ASSERT_TRUE(_event_queue.empty());\n}\n\nTEST_F(TestVst3xWrapper, TestCVOutput)\n{\n    SetUp(SUSHI_VST3_TEST_PLUGIN_PATH, PLUGIN_NAME);\n\n    auto status = _module_under_test->connect_cv_from_parameter(DELAY_PARAM_ID, 1);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    int index_unused;\n    auto param_queue = _accessor->process_data().outputParameterChanges->addParameterData(DELAY_PARAM_ID, index_unused);\n    ASSERT_TRUE(param_queue);\n    param_queue->addPoint(5, 0.75, index_unused);\n\n    _accessor->forward_params(_accessor->process_data());\n\n    ASSERT_FALSE(_event_queue.empty());\n    RtEvent event;\n    ASSERT_TRUE(_event_queue.pop(event));\n    ASSERT_EQ(RtEventType::CV_EVENT, event.type());\n    ASSERT_EQ(0, event.sample_offset());\n    ASSERT_EQ(1, event.cv_event()->cv_id());\n    ASSERT_FLOAT_EQ(0.75f, event.cv_event()->value());\n\n    ASSERT_TRUE(_event_queue.empty());\n}\n\nTEST_F(TestVst3xWrapper, TestStateHandling)\n{\n    ChunkSampleBuffer buffer(2);\n    SetUp(SUSHI_VST3_TEST_PLUGIN_PATH, PLUGIN_NAME);\n\n    auto desc = _module_under_test->parameter_from_name(\"Delay\");\n    ASSERT_TRUE(desc);\n\n    ProcessorState state;\n    state.set_bypass(true);\n    state.set_program(2);\n    state.add_parameter_change(desc->id(), 0.88);\n\n    auto status = _module_under_test->set_state(&state, false);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    // Check that new values are set and update notification is queued\n    EXPECT_FLOAT_EQ(0.88f, _module_under_test->parameter_value(desc->id()).second);\n    EXPECT_TRUE(_module_under_test->bypassed());\n    auto event = _host_control._dummy_dispatcher.retrieve_event();\n    ASSERT_TRUE(event->is_engine_notification());\n\n    // Test setting state with realtime running\n    state.set_bypass(false);\n    state.set_program(1);\n    state.add_parameter_change(desc->id(), 0.44);\n\n    status = _module_under_test->set_state(&state, true);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    event = _host_control._dummy_dispatcher.retrieve_event();\n    _module_under_test->process_event(event->to_rt_event(0));\n    _module_under_test->process_audio(buffer, buffer);\n\n    // Check that new values are set\n    EXPECT_FLOAT_EQ(0.44f, _module_under_test->parameter_value(desc->id()).second);\n    EXPECT_FALSE(_module_under_test->bypassed());\n\n    // Retrive the delete event and execute it to delete the RtState object\n    RtEvent rt_event;\n\n    int deleted_states = 0;\n    int notifications = 0;\n    while (_event_queue.pop(rt_event))\n    {\n        if (rt_event.type() == RtEventType::DELETE)\n        {\n            auto delete_event = Event::from_rt_event(rt_event, IMMEDIATE_PROCESS);\n            ASSERT_TRUE(delete_event);\n            static_cast<AsynchronousDeleteEvent*>(delete_event.get())->execute();\n            deleted_states++;\n        }\n        if (rt_event.type() == RtEventType::NOTIFY)\n        {\n            notifications++;\n        }\n    }\n\n    EXPECT_EQ(1, deleted_states);\n    EXPECT_EQ(1, notifications);\n}\n\nTEST_F(TestVst3xWrapper, TestMultipleStates)\n{\n    ChunkSampleBuffer buffer(2);\n    SetUp(SUSHI_VST3_TEST_PLUGIN_PATH, PLUGIN_NAME);\n\n    auto desc = _module_under_test->parameter_from_name(\"Delay\");\n    ASSERT_TRUE(desc);\n\n    ProcessorState state;\n\n    // Test setting state with realtime running\n    state.set_bypass(false);\n    state.add_parameter_change(desc->id(), 0.33);\n\n    auto status = _module_under_test->set_state(&state, true);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    auto event = _host_control._dummy_dispatcher.retrieve_event();\n    _module_under_test->process_event(event->to_rt_event(0));\n\n    // Send another state, also with manual event passing\n    state.add_parameter_change(desc->id(), 0.55);\n    status = _module_under_test->set_state(&state, true);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    EXPECT_FALSE(_accessor->state_change_queue().wasEmpty());\n\n    event = _host_control._dummy_dispatcher.retrieve_event();\n    _module_under_test->process_event(event->to_rt_event(0));\n\n    // Process twice and check that we got the value from the second state\n    _module_under_test->process_audio(buffer, buffer);\n    _module_under_test->process_audio(buffer, buffer);\n\n    EXPECT_FLOAT_EQ(0.55f, _module_under_test->parameter_value(desc->id()).second);\n    EXPECT_FALSE(_module_under_test->bypassed());\n\n    // Retrieve the delete events and execute them to delete the RtState objects\n    // Also make sure that a notification was sent for every state change\n    RtEvent rt_event;\n    int deleted_states = 0;\n    int notifications = 0;\n    while (_event_queue.pop(rt_event))\n    {\n        if (rt_event.type() == RtEventType::DELETE)\n        {\n            auto delete_event = Event::from_rt_event(rt_event, IMMEDIATE_PROCESS);\n            ASSERT_TRUE(delete_event);\n            static_cast<AsynchronousDeleteEvent*>(delete_event.get())->execute();\n            deleted_states++;\n        }\n\n        if (rt_event.type() == RtEventType::NOTIFY)\n        {\n            notifications++;\n        }\n    }\n\n    EXPECT_EQ(2, deleted_states);\n    EXPECT_EQ(2, notifications);\n}\n\nTEST_F(TestVst3xWrapper, TestBinaryStateSaving)\n{\n    ChunkSampleBuffer buffer(2);\n    SetUp(SUSHI_VST3_TEST_PLUGIN_PATH, PLUGIN_NAME);\n\n    auto desc = _module_under_test->parameter_from_name(\"Delay\");\n    ASSERT_TRUE(desc);\n    float prev_value = _module_under_test->parameter_value(desc->id()).second;\n\n    ProcessorState state = _module_under_test->save_state();\n    ASSERT_TRUE(state.has_binary_data());\n\n    // Set a parameter value, the re-apply the state\n    auto event = RtEvent::make_parameter_change_event(_module_under_test->id(), 0, desc->id(), 0.5f);\n    _module_under_test->process_event(event);\n    _module_under_test->process_audio(buffer, buffer);\n    _module_under_test->parameter_update_callback(_module_under_test.get(), 0);\n\n    EXPECT_NE(prev_value, _module_under_test->parameter_value(desc->id()).second);\n\n    auto status = _module_under_test->set_state(&state, false);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    // Check the value has reverted to the previous value\n    EXPECT_FLOAT_EQ(prev_value, _module_under_test->parameter_value(desc->id()).second);\n}\n\nTEST_F(TestVst3xWrapper, TestExtensionInterfaceEnumeration)\n{\n    Steinberg::IPtr<test_utils::Vst3TestPlugin> plugin = new test_utils::Vst3TestPlugin();\n    ASSERT_FALSE(plugin->host_app);\n\n    SetUp(plugin.get());\n\n    _module_under_test->set_enabled(true);\n    // Test that the wrapper correctly found the extension interfaces exposed by the plugin\n    ASSERT_TRUE(_module_under_test->enabled());\n    ASSERT_TRUE(_accessor->instance().processor_extension());\n    ASSERT_TRUE(_accessor->instance().controller_extension());\n\n    // Test that the plugin found the host extension interfaces\n    ASSERT_TRUE(plugin->host_app);\n    ASSERT_TRUE(plugin->host_extension);\n    ASSERT_TRUE(plugin->component_handler);\n    ASSERT_TRUE(plugin->component_handler_extension);\n}\n\nTEST_F(TestVst3xWrapper, TestStringProperties)\n{\n    Steinberg::IPtr<test_utils::Vst3TestPlugin> plugin = new test_utils::Vst3TestPlugin();\n    SetUp(plugin.get());\n    _module_under_test->set_enabled(true);\n\n    ASSERT_TRUE(plugin->component_handler_extension);\n\n    auto parameters = _module_under_test->all_parameters();\n    ASSERT_EQ(4, parameters.size());\n\n    ASSERT_EQ(test_utils::Vst3TestPlugin::PROPERTY_ID_1, parameters[2]->id());\n    ASSERT_EQ(ParameterType::STRING, parameters[2]->type());\n    ASSERT_EQ(\"property_1\", parameters[2]->name());\n    ASSERT_EQ(\"Property 1\", parameters[2]->label());\n\n    ASSERT_EQ(test_utils::Vst3TestPlugin::PROPERTY_ID_2, parameters[3]->id());\n    ASSERT_EQ(ParameterType::STRING, parameters[3]->type());\n    ASSERT_EQ(\"property_2\", parameters[3]->name());\n    ASSERT_EQ(\"Property 2\", parameters[3]->label());\n\n    // Set values\n    auto res = _module_under_test->set_property_value(test_utils::Vst3TestPlugin::PROPERTY_ID_1, \"new value\");\n    ASSERT_EQ(ProcessorReturnCode::OK, res);\n    // Property 2 is marked as read only\n    res = _module_under_test->set_property_value(test_utils::Vst3TestPlugin::PROPERTY_ID_2, \"three\");\n    ASSERT_EQ(ProcessorReturnCode::UNSUPPORTED_OPERATION, res);\n    // Pass an invalid id\n    res = _module_under_test->set_property_value(-34, \"jsgafupeewn\");\n    ASSERT_NE(ProcessorReturnCode::OK, res);\n\n    // Get the new values\n    ASSERT_EQ(\"new value\", _module_under_test->property_value(test_utils::Vst3TestPlugin::PROPERTY_ID_1).second);\n\n    // Check the events that should have been posted by the set request\n    auto event = _host_control._dummy_dispatcher.retrieve_event();\n    ASSERT_TRUE(event);\n    ASSERT_TRUE(event->is_property_change_notification());\n    auto typed_event = static_cast<PropertyChangeNotificationEvent*>(event.get());\n    ASSERT_EQ(\"new value\", typed_event->value());\n    ASSERT_EQ(_module_under_test->id(), typed_event->processor_id());\n    ASSERT_EQ(test_utils::Vst3TestPlugin::PROPERTY_ID_1, typed_event->property_id());\n    // There should be 1 more event\n    event = _host_control._dummy_dispatcher.retrieve_event();\n    ASSERT_TRUE(event);\n    ASSERT_TRUE(event->maps_to_rt_event());\n    auto rt_event = event->to_rt_event(0);\n    ASSERT_EQ(RtEventType::STRING_PROPERTY_CHANGE, rt_event.type());\n    auto typed_rt_event = rt_event.property_change_event();\n    ASSERT_EQ(\"new value\", *typed_rt_event->value());\n    ASSERT_EQ(test_utils::Vst3TestPlugin::PROPERTY_ID_1, typed_rt_event->param_id());\n    ASSERT_EQ(_module_under_test->id(), typed_rt_event->processor_id());\n\n    // clean up manually\n    delete typed_rt_event->deletable_value();\n\n    // Queue should now be empty\n    ASSERT_FALSE(_host_control._dummy_dispatcher.got_event());\n\n    // Test notification from inside the plugin\n    plugin->component_handler_extension->notifyPropertyValueChange(test_utils::Vst3TestPlugin::PROPERTY_ID_2,\n                                                                   elk::PropertyValue(\"seven\", 5));\n    event = _host_control._dummy_dispatcher.retrieve_event();\n    ASSERT_TRUE(event);\n    ASSERT_TRUE(event->is_property_change_notification());\n    typed_event = static_cast<PropertyChangeNotificationEvent*>(event.get());\n    ASSERT_EQ(\"seven\", typed_event->value());\n    ASSERT_EQ(_module_under_test->id(), typed_event->processor_id());\n    ASSERT_EQ(test_utils::Vst3TestPlugin::PROPERTY_ID_2, typed_event->property_id());\n\n    // Queue should now be empty again\n    ASSERT_FALSE(_host_control._dummy_dispatcher.got_event());\n}\n\nTEST_F(TestVst3xWrapper, TestAsyncWork)\n{\n    Steinberg::IPtr<test_utils::Vst3TestPlugin> plugin = new test_utils::Vst3TestPlugin();\n    SetUp(plugin.get());\n    _module_under_test->set_enabled(true);\n    ASSERT_TRUE(plugin->component_handler_extension);\n\n    // Fake an audio thread environment\n    twine::ThreadRtFlag flag;\n    ASSERT_TRUE(plugin->send_async_work_request());\n    RtEvent rt_event;\n    ASSERT_FALSE(_event_queue.empty());\n    ASSERT_TRUE(_event_queue.pop(rt_event));\n    ASSERT_EQ(RtEventType::ASYNC_WORK, rt_event.type());\n    // Manually execute the event\n    auto typed_event = rt_event.async_work_event();\n    auto res = typed_event->callback()(typed_event->callback_data(), typed_event->event_id());\n    auto return_event = RtEvent::make_async_work_completion_event(_module_under_test->id(), typed_event->event_id(), res);\n    _module_under_test->process_event(return_event);\n\n    ASSERT_EQ(plugin->_last_async_id, plugin->_last_async_id_received);\n    ASSERT_EQ(typed_event->event_id() + test_utils::CALLBACK_ID, plugin->_last_async_status);\n}\n\nclass TestVst3xUtils : public ::testing::Test\n{\nprotected:\n    TestVst3xUtils() {}\n};\n\nTEST_F(TestVst3xUtils, TestNoteOnConversion)\n{\n    auto event = RtEvent::make_note_on_event(ObjectId(0), 12, 1, 45, 0.5f);\n    auto vst_event = convert_note_on_event(event.keyboard_event());\n    EXPECT_EQ(0, vst_event.busIndex);\n    EXPECT_EQ(12, vst_event.sampleOffset);\n    EXPECT_EQ(0, vst_event.ppqPosition);\n    EXPECT_EQ(0, vst_event.flags);\n    EXPECT_EQ(Steinberg::Vst::Event::kNoteOnEvent, vst_event.type);\n    EXPECT_EQ(1, vst_event.noteOn.channel);\n    EXPECT_EQ(45, vst_event.noteOn.pitch);\n    EXPECT_FLOAT_EQ(0.0f, vst_event.noteOn.tuning);\n    EXPECT_FLOAT_EQ(0.5f, vst_event.noteOn.velocity);\n    EXPECT_EQ(0, vst_event.noteOn.length);\n    EXPECT_EQ(-1, vst_event.noteOn.noteId);\n}\n\nTEST_F(TestVst3xUtils, TestNoteOffConversion)\n{\n    auto event = RtEvent::make_note_off_event(ObjectId(0), 12, 1, 45, 0.5f);\n    auto vst_event = convert_note_off_event(event.keyboard_event());\n    EXPECT_EQ(0, vst_event.busIndex);\n    EXPECT_EQ(12, vst_event.sampleOffset);\n    EXPECT_EQ(0, vst_event.ppqPosition);\n    EXPECT_EQ(0, vst_event.flags);\n    EXPECT_EQ(Steinberg::Vst::Event::kNoteOffEvent, vst_event.type);\n    EXPECT_EQ(1, vst_event.noteOff.channel);\n    EXPECT_EQ(45, vst_event.noteOff.pitch);\n    EXPECT_FLOAT_EQ(0.0f, vst_event.noteOff.tuning);\n    EXPECT_FLOAT_EQ(0.5f, vst_event.noteOff.velocity);\n    EXPECT_EQ(-1, vst_event.noteOff.noteId);\n}\n\nTEST_F(TestVst3xUtils, TestAftertouchConversion)\n{\n    auto event = RtEvent::make_note_aftertouch_event(ObjectId(0), 12, 1, 45, 0.5f);\n    auto vst_event = convert_aftertouch_event(event.keyboard_event());\n    EXPECT_EQ(0, vst_event.busIndex);\n    EXPECT_EQ(12, vst_event.sampleOffset);\n    EXPECT_EQ(0, vst_event.ppqPosition);\n    EXPECT_EQ(0, vst_event.flags);\n    EXPECT_EQ(Steinberg::Vst::Event::kPolyPressureEvent, vst_event.type);\n    EXPECT_EQ(1, vst_event.polyPressure.channel);\n    EXPECT_EQ(45, vst_event.polyPressure.pitch);\n    EXPECT_FLOAT_EQ(0.5f, vst_event.polyPressure.pressure);\n    EXPECT_EQ(-1, vst_event.polyPressure.noteId);\n}\n\nTEST(TestVst3xUtilFunctions, TestMakeSafeFolderName)\n{\n    EXPECT_EQ(\"il_&_al_file n__me\", make_safe_folder_name(\"il*&?al_file n<>me\"));\n}\n\nTEST(TestVst3xUtilFunctions, TestIsHidden)\n{\n    auto entry = std::filesystem::directory_entry(std::filesystem::absolute(SUSHI_VST3_TEST_PLUGIN_PATH));\n    EXPECT_FALSE(is_hidden(entry));\n\n#ifndef _MSC_VER // Currently not testing this under windows as git doesn't preserve file properties across platforms\n    auto path = std::filesystem::path(test_utils::get_data_dir_path()).append(\".hidden_file.txt\");\n    entry = std::filesystem::directory_entry(path);\n    EXPECT_TRUE(is_hidden(entry));\n#endif\n}\n\nTEST(TestVst3xUtilFunctions, TestScanForPresets)\n{\n    // This is more of a smoke test, it will likely return 0 results,\n    // but we exercise the code to catch exceptions or crashes.\n    auto paths = scan_for_presets(\"Elk Audio\", \"Elk Wire\");\n    ASSERT_GE(paths.size(), 0u);\n}\n\nTEST(TestVst3xUtilFunctions, TestGetExecutablePath)\n{\n    auto path = get_executable_path();\n    ASSERT_FALSE(path.empty());\n    ASSERT_FALSE(path.filename().empty());\n    EXPECT_TRUE(path.is_absolute());\n    EXPECT_EQ(UNITTEST_EXE, path.filename().string());\n}\n\nTEST(TestVst3xUtilFunctions, TestGetPlatformLocations)\n{\n    auto locations = get_platform_locations();\n    EXPECT_EQ(4, locations.size());\n    for (const auto& path : locations)\n    {\n        EXPECT_FALSE(path.empty());\n    }\n}\n\nTEST(TestVst3xUtilFunctions, TestExtractPresetName)\n{\n    EXPECT_EQ(\"lately bass\", extract_preset_name(std::filesystem::path(\"/etc/presets/lately bass.vstpreset\")));\n    // This should not crash or throw\n    EXPECT_EQ(\"\", extract_preset_name(std::filesystem::path(\"etc/presets/\")));\n}"
  },
  {
    "path": "test/unittests/plugins/arpeggiator_plugin_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"library/rt_event_fifo.h\"\n#include \"test_utils/host_control_mockup.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"plugins/arpeggiator_plugin.cpp\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::arpeggiator_plugin;\n\nconstexpr float TEST_SAMPLERATE = 48000;\n\nclass TestArpeggiator : public ::testing::Test\n{\nprotected:\n    TestArpeggiator() = default;\n\n    Arpeggiator _module_under_test;\n};\n\n\nTEST_F(TestArpeggiator, TestOperation)\n{\n    _module_under_test.add_note(10);\n    _module_under_test.add_note(14);\n    _module_under_test.add_note(17);\n    _module_under_test.set_range(2);\n\n    /* Play chord in 2 octaves */\n    EXPECT_EQ(10, _module_under_test.next_note());\n    EXPECT_EQ(14, _module_under_test.next_note());\n    EXPECT_EQ(17, _module_under_test.next_note());\n    EXPECT_EQ(22, _module_under_test.next_note());\n    EXPECT_EQ(26, _module_under_test.next_note());\n    EXPECT_EQ(29, _module_under_test.next_note());\n    EXPECT_EQ(10, _module_under_test.next_note());\n\n    _module_under_test.remove_note(14);\n    _module_under_test.set_range(1);\n\n    EXPECT_EQ(17, _module_under_test.next_note());\n    EXPECT_EQ(10, _module_under_test.next_note());\n}\n\n\nTEST_F(TestArpeggiator, TestHold)\n{\n    _module_under_test.set_range(2);\n    _module_under_test.add_note(15);\n    _module_under_test.remove_note(15);\n\n    EXPECT_EQ(15, _module_under_test.next_note());\n    EXPECT_EQ(27, _module_under_test.next_note());\n    EXPECT_EQ(15, _module_under_test.next_note());\n\n    _module_under_test.add_note(14);\n    _module_under_test.add_note(17);\n\n    EXPECT_EQ(17, _module_under_test.next_note());\n    EXPECT_EQ(26, _module_under_test.next_note());\n    EXPECT_EQ(29, _module_under_test.next_note());\n    EXPECT_EQ(14, _module_under_test.next_note());\n\n    _module_under_test.remove_note(17);\n    _module_under_test.remove_note(14);\n\n    EXPECT_EQ(26, _module_under_test.next_note());\n    EXPECT_EQ(14, _module_under_test.next_note());\n}\n\n\nclass TestArpeggiatorPlugin : public ::testing::Test\n{\nprotected:\n    TestArpeggiatorPlugin() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = new arpeggiator_plugin::ArpeggiatorPlugin(_host_control.make_host_control_mockup(TEST_SAMPLERATE));\n        ProcessorReturnCode status = _module_under_test->init(TEST_SAMPLERATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n        _module_under_test->set_event_output(&_fifo);\n    }\n\n    void TearDown() override\n    {\n        delete _module_under_test;\n    }\n\n    RtSafeRtEventFifo _fifo;\n    HostControlMockup _host_control;\n    arpeggiator_plugin::ArpeggiatorPlugin* _module_under_test;\n};\n\n\nTEST_F(TestArpeggiatorPlugin, TestOutput)\n{\n    ChunkSampleBuffer buffer;\n    auto event = RtEvent::make_note_on_event(0, 0, 0, 50, 1.0f);\n    _module_under_test->process_event(event);\n    _host_control._transport.set_tempo(120.0f, false);\n    _host_control._transport.set_playing_mode(PlayingMode::PLAYING, false);\n    _host_control._transport.set_time(std::chrono::milliseconds(0), 0);\n\n    ASSERT_TRUE(_fifo.empty());\n    /* 1/8 notes at 120 bpm equals 4 notes/sec, @48000 results in  at least\n     * 12000 samples and 1/4 sec to catch one note, use the host control to\n     * fast forward the time directly */\n    _host_control._transport.set_time(std::chrono::milliseconds(250), 12000);\n    _module_under_test->process_audio(buffer, buffer);\n    RtEvent e;\n    bool got_event = _fifo.pop(e);\n    ASSERT_TRUE(got_event);\n    ASSERT_EQ(_module_under_test->id(), e.processor_id());\n    ASSERT_EQ(RtEventType::NOTE_OFF, e.type());\n    _fifo.pop(e);\n    ASSERT_EQ(_module_under_test->id(), e.processor_id());\n    ASSERT_EQ(RtEventType::NOTE_ON, e.type());\n    ASSERT_EQ(50, e.keyboard_event()->note());\n    ASSERT_TRUE(_fifo.empty());\n}\n"
  },
  {
    "path": "test/unittests/plugins/brickworks_simple_synth_test.cpp",
    "content": "#include <algorithm>\n#include <memory>\n#include <random>\n\n#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/host_control_mockup.h\"\n\n#include \"plugins/brickworks/simple_synth_plugin.cpp\"\n\nnamespace sushi::internal::simple_synth_plugin\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(SimpleSynthPlugin& plugin) : _plugin(plugin) {}\n\n    [[nodiscard]] FloatParameterValue* decay()\n    {\n        return _plugin._decay;\n    }\n\n    [[nodiscard]] FloatParameterValue* release()\n    {\n        return _plugin._release;\n    }\n\nprivate:\n    SimpleSynthPlugin& _plugin;\n};\n\n}\n\nusing namespace sushi;\nusing namespace sushi::internal::simple_synth_plugin;\n\nconstexpr float TEST_SAMPLERATE = 48000;\nconstexpr int   TEST_CHANNEL_COUNT = 2;\n\n\nclass TestSimpleSynthPlugin : public ::testing::Test\n{\nprotected:\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<SimpleSynthPlugin>(_host_control.make_host_control_mockup(TEST_SAMPLERATE));\n\n        _accessor = std::make_unique<sushi::internal::simple_synth_plugin::Accessor>(*_module_under_test);\n\n        ProcessorReturnCode status = _module_under_test->init(TEST_SAMPLERATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n        _module_under_test->set_channels(0, TEST_CHANNEL_COUNT);\n        _module_under_test->set_enabled(true);\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<SimpleSynthPlugin> _module_under_test;\n\n    std::unique_ptr<sushi::internal::simple_synth_plugin::Accessor> _accessor;\n};\n\nTEST_F(TestSimpleSynthPlugin, TestInstantiation)\n{\n    ChunkSampleBuffer in_buffer(TEST_CHANNEL_COUNT);\n    ChunkSampleBuffer out_buffer(TEST_CHANNEL_COUNT);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(0.0f, out_buffer);\n}\n\nTEST_F(TestSimpleSynthPlugin, TestProcessing)\n{\n    ChunkSampleBuffer in_buffer(TEST_CHANNEL_COUNT);\n    ChunkSampleBuffer out_buffer(TEST_CHANNEL_COUNT);\n\n    RtEvent note_on = RtEvent::make_note_on_event(0, 0, 0, 60, 1.0f);\n    _module_under_test->process_event(note_on);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_non_null(out_buffer);\n\n    RtEvent note_off = RtEvent::make_note_off_event(0, 0, 0, 60, 1.0f);\n    _module_under_test->process_event(note_off);\n    // give some time for release to act\n    float total_release = _accessor->decay()->processed_value() + _accessor->release()->processed_value();\n    int release_buffers = static_cast<int>((total_release * TEST_SAMPLERATE) / AUDIO_CHUNK_SIZE) + 1;\n    for (int i = 0; i < release_buffers; i++)\n    {\n        _module_under_test->process_audio(in_buffer, out_buffer);\n    }\n    test_utils::assert_buffer_value(0.0f, out_buffer);\n}\n\nTEST_F(TestSimpleSynthPlugin, TestNoteOnandOffSameCallback)\n{\n    ChunkSampleBuffer in_buffer(TEST_CHANNEL_COUNT);\n    ChunkSampleBuffer out_buffer(TEST_CHANNEL_COUNT);\n\n    RtEvent note_on = RtEvent::make_note_on_event(0, 0, 0, 60, 1.0f);\n    _module_under_test->process_event(note_on);\n    RtEvent note_off = RtEvent::make_note_off_event(0, 1, 0, 60, 1.0f);\n    _module_under_test->process_event(note_off);\n    note_on = RtEvent::make_note_on_event(0, 2, 0, 60, 1.0f);\n    _module_under_test->process_event(note_on);\n\n    _module_under_test->process_event(note_on);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_non_null(out_buffer);\n}\n\nTEST_F(TestSimpleSynthPlugin, TestNoNaNsUnderStress)\n{\n    // just go crazy with NoteONs + parameter changes and verify that no NaNs are generated\n\n    ChunkSampleBuffer in_buffer(TEST_CHANNEL_COUNT);\n    ChunkSampleBuffer out_buffer(TEST_CHANNEL_COUNT);\n\n    std::ranlux24 rand_dev;\n    std::uniform_real_distribution<float> dist{0.0f, 1.0f};\n    std::uniform_int_distribution<int> note_dist{0, 127};\n\n\n    for (int i = 0; i < 128; i++)\n    {\n        for (auto& pd : _module_under_test->all_parameters())\n        {\n            _module_under_test->process_event(RtEvent::make_parameter_change_event(_module_under_test->id(),\n                                                                                   0,\n                                                                                   pd->id(),\n                                                                                   dist(rand_dev)));\n        }\n        RtEvent note_on = RtEvent::make_note_on_event(0, 0, 0, note_dist(rand_dev), 1.0f);\n        _module_under_test->process_event(note_on);\n\n        _module_under_test->process_audio(in_buffer, out_buffer);\n        test_utils::assert_buffer_not_nan(out_buffer);\n    }\n}\n\n"
  },
  {
    "path": "test/unittests/plugins/control_to_cv_plugin_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"plugins/control_to_cv_plugin.cpp\"\n#include \"library/rt_event_fifo.h\"\n#include \"test_utils/host_control_mockup.h\"\n\n#include <iostream>\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::control_to_cv_plugin;\n\nconstexpr float TEST_SAMPLE_RATE = 44100;\n\nTEST(ControlToCvPluginTestExternalFunctions, TestPitchToCv)\n{\n    EXPECT_FLOAT_EQ(0.5f, pitch_to_cv(60.0f));\n    // TODO - Add more test when we have a proper midi note to scaling function in place\n}\n\nclass ControlToCvPluginTest : public ::testing::Test\n{\nprotected:\n    ControlToCvPluginTest() = default;\n\n    void SetUp() override\n    {\n        _module_under_test.init(TEST_SAMPLE_RATE);\n        _module_under_test.set_event_output(&_event_output);\n    }\n\n    HostControlMockup _host_control;\n    ControlToCvPlugin _module_under_test{_host_control.make_host_control_mockup(TEST_SAMPLE_RATE)};\n    RtEventFifo<10>   _event_output;\n    ChunkSampleBuffer _audio_buffer{2};\n};\n\nTEST_F(ControlToCvPluginTest, TestMonophonicMode)\n{\n    constexpr int PITCH_CV = 1;\n\n    // Only connect 1 pitch output\n    auto status = _module_under_test.connect_cv_from_parameter(_module_under_test.parameter_from_name(\"pitch_0\")->id(), PITCH_CV);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    status = _module_under_test.connect_gate_from_processor(0, 0, 0);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    ASSERT_EQ(1, _event_output.size());\n    EXPECT_EQ(RtEventType::CV_EVENT, _event_output[0].type());\n    _event_output.clear();\n\n    // Send a note on message\n    auto event = RtEvent::make_note_on_event(_module_under_test.id(), 0, 0, 60, 1.0f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n\n    // We should receive 1 gate high and 1 pitch CV event\n    ASSERT_EQ(2, _event_output.size());\n    EXPECT_EQ(RtEventType::GATE_EVENT, _event_output[0].type());\n    EXPECT_TRUE(_event_output[0].gate_event()->value());\n    EXPECT_EQ(0, _event_output[0].gate_event()->gate_no());\n\n    EXPECT_EQ(RtEventType::CV_EVENT, _event_output[1].type());\n    EXPECT_EQ(PITCH_CV, _event_output[1].cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.5f, _event_output[1].cv_event()->value());\n    _event_output.clear();\n\n    // Send another note on message\n    event = RtEvent::make_note_on_event(_module_under_test.id(), 0, 0, 48, 1.0f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    // We should receive 1 pitch CV event with lower pitch\n    ASSERT_EQ(1, _event_output.size());\n    EXPECT_EQ(RtEventType::CV_EVENT, _event_output[0].type());\n    EXPECT_EQ(PITCH_CV, _event_output[0].cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.4f, _event_output[0].cv_event()->value());\n    _event_output.clear();\n\n    // Enable retrigger and send yet another note on\n    event = RtEvent::make_parameter_change_event(_module_under_test.id(), 0, _module_under_test.parameter_from_name(\"retrigger_enabled\")->id(), 1.0f);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_note_on_event(_module_under_test.id(), 0, 0, 66, 1.0f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n\n    // We should receive 1 gate low and 1 pitch CV event\n    ASSERT_EQ(2, _event_output.size());\n    EXPECT_EQ(RtEventType::GATE_EVENT, _event_output[0].type());\n    EXPECT_FALSE(_event_output[0].gate_event()->value());\n    EXPECT_EQ(0, _event_output[0].gate_event()->gate_no());\n\n    EXPECT_EQ(RtEventType::CV_EVENT, _event_output[1].type());\n    EXPECT_EQ(PITCH_CV, _event_output[1].cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.55f, _event_output[1].cv_event()->value());\n    _event_output.clear();\n\n    // And the gate high should come the next buffer\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    ASSERT_EQ(2, _event_output.size());\n    EXPECT_EQ(RtEventType::GATE_EVENT, _event_output[0].type());\n    EXPECT_TRUE(_event_output[0].gate_event()->value());\n    EXPECT_EQ(0, _event_output[0].gate_event()->gate_no());\n\n    EXPECT_EQ(RtEventType::CV_EVENT, _event_output[1].type());\n    EXPECT_EQ(PITCH_CV, _event_output[1].cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.55f, _event_output[1].cv_event()->value());\n    _event_output.clear();\n}\n\nTEST_F(ControlToCvPluginTest, TestPolyphonicMode)\n{\n    constexpr int PITCH_CV_1 = 0;\n    constexpr int PITCH_CV_2 = 1;\n    constexpr int VEL_CV_1 = 2;\n    constexpr int VEL_CV_2 = 3;\n\n    // Use 2 pitch and 2 velocity outputs\n    auto status = _module_under_test.connect_cv_from_parameter(_module_under_test.parameter_from_name(\"pitch_0\")->id(), PITCH_CV_1);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    status = _module_under_test.connect_cv_from_parameter(_module_under_test.parameter_from_name(\"pitch_1\")->id(), PITCH_CV_2);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    status = _module_under_test.connect_cv_from_parameter(_module_under_test.parameter_from_name(\"velocity_0\")->id(), VEL_CV_1);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    status = _module_under_test.connect_cv_from_parameter(_module_under_test.parameter_from_name(\"velocity_1\")->id(), VEL_CV_2);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    status = _module_under_test.connect_gate_from_processor(0, 0, 0);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    status = _module_under_test.connect_gate_from_processor(1, 0, 1);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    auto event = RtEvent::make_parameter_change_event(_module_under_test.id(), 0, _module_under_test.parameter_from_name(\"polyphony\")->id(), 0.5f);\n    _module_under_test.process_event(event);\n\n    event = RtEvent::make_parameter_change_event(_module_under_test.id(), 0, _module_under_test.parameter_from_name(\"send_velocity\")->id(), 1.0f);\n    _module_under_test.process_event(event);\n\n    // Send 2 note on messages\n    event = RtEvent::make_note_on_event(_module_under_test.id(), 0, 0, 60, 0.75f);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_note_on_event(_module_under_test.id(), 0, 0, 48, 0.5f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n\n    ASSERT_EQ(6, _event_output.size());\n    auto e = _event_output.pop();\n    EXPECT_EQ(RtEventType::GATE_EVENT, e.type());\n    EXPECT_TRUE(e.gate_event()->value());\n    EXPECT_EQ(0, e.gate_event()->gate_no());\n\n    e = _event_output.pop();\n    EXPECT_TRUE(e.gate_event()->value());\n    EXPECT_EQ(1, e.gate_event()->gate_no());\n\n    e = _event_output.pop();\n    EXPECT_EQ(RtEventType::CV_EVENT, e.type());\n    EXPECT_EQ(PITCH_CV_1, e.cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.50f, e.cv_event()->value());\n\n    e = _event_output.pop();\n    EXPECT_EQ(RtEventType::CV_EVENT, e.type());\n    EXPECT_EQ(PITCH_CV_2, e.cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.40f, e.cv_event()->value());\n\n    e = _event_output.pop();\n    EXPECT_EQ(RtEventType::CV_EVENT, e.type());\n    EXPECT_EQ(VEL_CV_1, e.cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.75f, e.cv_event()->value());\n\n    e = _event_output.pop();\n    EXPECT_EQ(RtEventType::CV_EVENT, e.type());\n    EXPECT_EQ(VEL_CV_2, e.cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.50f, e.cv_event()->value());\n\n    _event_output.clear();\n\n    // Send 1 more note on message\n    event = RtEvent::make_note_on_event(_module_under_test.id(), 0, 0, 78, 0.4f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n\n    // Should not result in any more gate low events, but pitch cv 1 should change.\n    ASSERT_EQ(4, _event_output.size());\n\n    e = _event_output.pop();\n    EXPECT_EQ(RtEventType::CV_EVENT, e.type());\n    EXPECT_EQ(PITCH_CV_1, e.cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.65f, e.cv_event()->value());\n\n    e = _event_output.pop();\n    EXPECT_EQ(RtEventType::CV_EVENT, e.type());\n    EXPECT_EQ(PITCH_CV_2, e.cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.4f, e.cv_event()->value());\n\n    e = _event_output.pop();\n    EXPECT_EQ(RtEventType::CV_EVENT, e.type());\n    EXPECT_EQ(VEL_CV_1, e.cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.4f, e.cv_event()->value());\n\n    e = _event_output.pop();\n    EXPECT_EQ(RtEventType::CV_EVENT, e.type());\n    EXPECT_EQ(VEL_CV_2, e.cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.5f, e.cv_event()->value());\n\n    _event_output.clear();\n\n    // Send 3 note off messages\n    event = RtEvent::make_note_off_event(_module_under_test.id(), 0, 0, 78, 0.5f);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_note_off_event(_module_under_test.id(), 0, 0, 48, 0.5f);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_note_off_event(_module_under_test.id(), 0, 0, 60, 0.5f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n\n    ASSERT_EQ(6, _event_output.size());\n    e = _event_output.pop();\n    EXPECT_EQ(RtEventType::GATE_EVENT, e.type());\n    EXPECT_FALSE(e.gate_event()->value());\n    EXPECT_EQ(0, e.gate_event()->gate_no());\n\n    e = _event_output.pop();\n    EXPECT_EQ(RtEventType::GATE_EVENT, e.type());\n    EXPECT_FALSE(e.gate_event()->value());\n    EXPECT_EQ(1, e.gate_event()->gate_no());\n    _event_output.clear();\n}\n\nTEST_F(ControlToCvPluginTest, TestPitchBend)\n{\n    constexpr int PITCH_CV = 2;\n    auto status = _module_under_test.connect_cv_from_parameter(_module_under_test.parameter_from_name(\"pitch_0\")->id(), PITCH_CV);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    status = _module_under_test.connect_gate_from_processor(0, 0, 0);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    // Send a note on message and a pitch bend message\n    auto event = RtEvent::make_note_on_event(_module_under_test.id(), 0, 0, 48, 0.5f);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_pitch_bend_event(_module_under_test.id(), 0, 0, 0.5f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n\n    // We should receive 1 events, gate and pitch,\n    ASSERT_EQ(2, _event_output.size());\n    auto e = _event_output[1];\n    EXPECT_EQ(RtEventType::CV_EVENT, e.type());\n    EXPECT_EQ(PITCH_CV, e.cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.45f, e.cv_event()->value());\n    _event_output.clear();\n\n    // Set the tune parameters up 1 octave\n    event = RtEvent::make_parameter_change_event(_module_under_test.id(), 0, _module_under_test.parameter_from_name(\"tune\")->id(), 0.75f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n\n    // We should receive 1 pitch event,\n    ASSERT_EQ(1, _event_output.size());\n    e = _event_output[0];\n    EXPECT_EQ(RtEventType::CV_EVENT, e.type());\n    EXPECT_EQ(PITCH_CV, e.cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.55f, e.cv_event()->value());\n    _event_output.clear();\n}\n\nTEST_F(ControlToCvPluginTest, TestModulation)\n{\n    constexpr int PITCH_CV = 0;\n    constexpr int MOD_CV = 1;\n    // Use 2 pitch and 2 velocity outputs\n    auto status = _module_under_test.connect_cv_from_parameter(_module_under_test.parameter_from_name(\"modulation\")->id(), MOD_CV);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    status = _module_under_test.connect_cv_from_parameter(_module_under_test.parameter_from_name(\"pitch_0\")->id(), PITCH_CV);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n    status = _module_under_test.connect_gate_from_processor(0, 0, 0);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    auto event = RtEvent::make_parameter_change_event(_module_under_test.id(), 0, _module_under_test.parameter_from_name(\"send_modulation\")->id(), 2.0f);\n    _module_under_test.process_event(event);\n\n    // Send 2 note on messages\n    event = RtEvent::make_kb_modulation_event(_module_under_test.id(), 0, 0, 0.5f);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_note_on_event(_module_under_test.id(), 0, 0, 48, 0.1f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n\n    // We should receive 3 events, gate, pitch and modulation cv\n    ASSERT_EQ(3, _event_output.size());\n\n    auto e = _event_output[2];\n    EXPECT_EQ(RtEventType::CV_EVENT, e.type());\n    EXPECT_EQ(MOD_CV, e.cv_event()->cv_id());\n    EXPECT_FLOAT_EQ(0.5f, e.cv_event()->value());\n}\n\n"
  },
  {
    "path": "test/unittests/plugins/cv_to_control_plugin_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"plugins/cv_to_control_plugin.cpp\"\n#include \"library/rt_event_fifo.h\"\n#include \"test_utils/host_control_mockup.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::cv_to_control_plugin;\n\nconstexpr float TEST_SAMPLE_RATE = 44100;\n\nTEST(CvToControlPluginTestExternalFunctions, TestCvToPitch)\n{\n    auto [note, fraction] = cv_to_pitch(0.502f);\n    EXPECT_EQ(60, note);\n    EXPECT_FLOAT_EQ(0.23999786f, fraction);\n}\n\nclass CvToControlPluginTest : public ::testing::Test\n{\nprotected:\n    CvToControlPluginTest() = default;\n\n    void SetUp() override\n    {\n        _module_under_test.init(TEST_SAMPLE_RATE);\n        _module_under_test.set_event_output(&_event_output);\n    }\n\n    HostControlMockup _host_control;\n    CvToControlPlugin _module_under_test{_host_control.make_host_control_mockup(TEST_SAMPLE_RATE)};\n    RtEventFifo<10>   _event_output;\n    ChunkSampleBuffer _audio_buffer{2};\n};\n\nTEST_F(CvToControlPluginTest, TestMonophonicMode)\n{\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    EXPECT_TRUE(_event_output.empty());\n\n    // Set pitch parameter and send a gate high event, we should receive a note on event\n    auto event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"polyphony\")->id(), 0);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"pitch_0\")->id(), 0.5);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_note_on_event(0, 0, 0, 0, 1.0f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    EXPECT_FALSE(_event_output.empty());\n    auto recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::NOTE_ON, recv_event.type());\n    EXPECT_EQ(60, recv_event.keyboard_event()->note());\n    EXPECT_FLOAT_EQ(1.0f, recv_event.keyboard_event()->velocity());\n    EXPECT_TRUE(_event_output.empty());\n\n    // Change the pitch enough to trigger a new note on\n    event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"pitch_0\")->id(), 0.51f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    EXPECT_FALSE(_event_output.empty());\n    recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::NOTE_ON, recv_event.type());\n    EXPECT_EQ(61, recv_event.keyboard_event()->note());\n    EXPECT_TRUE(_event_output.empty());\n\n    // The note off should come the next buffer, to enable soft synths to do legato\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    EXPECT_FALSE(_event_output.empty());\n    recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::NOTE_OFF, recv_event.type());\n    EXPECT_EQ(60, recv_event.keyboard_event()->note());\n\n    // Now Send a gate low event, and we should receive a note off with the new note\n    event = RtEvent::make_note_off_event(0, 0, 0, 0, 1.0f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::NOTE_OFF, recv_event.type());\n    EXPECT_EQ(61, recv_event.keyboard_event()->note());\n    EXPECT_TRUE(_event_output.empty());\n}\n\nTEST_F(CvToControlPluginTest, TestPitchBendMode)\n{\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    EXPECT_TRUE(_event_output.empty());\n\n    // Set pitch parameter and send a gate high event, we should receive a note on event and a pitch bend\n    auto event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"polyphony\")->id(), 0);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"pitch_bend_enabled\")->id(), 1);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"pitch_0\")->id(), 0.501f);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_note_on_event(0, 0, 0, 0, 1.0f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    EXPECT_FALSE(_event_output.empty());\n    auto recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::NOTE_ON, recv_event.type());\n    EXPECT_EQ(60, recv_event.keyboard_event()->note());\n    EXPECT_FLOAT_EQ(1.0f, recv_event.keyboard_event()->velocity());\n    recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::PITCH_BEND, recv_event.type());\n    float initial_pb = recv_event.keyboard_common_event()->value();\n    EXPECT_GT(initial_pb, 0.0f);\n    EXPECT_TRUE(_event_output.empty());\n\n    // Change the pitch up ~one semitone, this should not send a new note on, only a pitch bend with a higher value\n    event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"pitch_0\")->id(), 0.51f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    EXPECT_FALSE(_event_output.empty());\n    recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::PITCH_BEND, recv_event.type());\n    EXPECT_GT(recv_event.keyboard_common_event()->value() , initial_pb);\n    EXPECT_TRUE(_event_output.empty());\n}\n\nTEST_F(CvToControlPluginTest, TestVelocity)\n{\n    // Set pitch parameter and send a gate high event, we should receive a note on event and a pitch bend\n    auto event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"velocity_enabled\")->id(), 1);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"pitch_0\")->id(), 0.5f);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"velocity_0\")->id(), 0.75f);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_note_on_event(0, 0, 0, 0, 1.0f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    EXPECT_FALSE(_event_output.empty());\n    auto recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::NOTE_ON, recv_event.type());\n    EXPECT_EQ(60, recv_event.keyboard_event()->note());\n    EXPECT_FLOAT_EQ(0.75f, recv_event.keyboard_event()->velocity());\n    EXPECT_TRUE(_event_output.empty());\n}\n\nTEST_F(CvToControlPluginTest, TestPolyphony)\n{\n    // Set pitch parameter and send a gate high event, we should receive a note on event\n    auto event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"polyphony\")->id(), 4);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"pitch_0\")->id(), 0.5);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_note_on_event(0, 0, 0, 0, 1.0f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    EXPECT_FALSE(_event_output.empty());\n    auto recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::NOTE_ON, recv_event.type());\n    EXPECT_EQ(60, recv_event.keyboard_event()->note());\n    EXPECT_FLOAT_EQ(1.0f, recv_event.keyboard_event()->velocity());\n    EXPECT_TRUE(_event_output.empty());\n\n    // Sent 2 new gate ons\n    event = RtEvent::make_note_on_event(0, 0, 0, 1, 1.0f);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_note_on_event(0, 0, 0, 2, 1.0f);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_parameter_change_event(0, 0, _module_under_test.parameter_from_name(\"pitch_1\")->id(), 0.3f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::NOTE_ON, recv_event.type());\n    recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::NOTE_ON, recv_event.type());\n    EXPECT_TRUE(_event_output.empty());\n\n    // Sent 2 new gate offs\n    event = RtEvent::make_note_off_event(0, 0, 0, 0, 1.0f);\n    _module_under_test.process_event(event);\n    event = RtEvent::make_note_off_event(0, 0, 0, 2, 1.0f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::NOTE_OFF, recv_event.type());\n    recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::NOTE_OFF, recv_event.type());\n    EXPECT_TRUE(_event_output.empty());\n\n    event = RtEvent::make_note_off_event(0, 0, 0, 1, 1.0f);\n    _module_under_test.process_event(event);\n    _module_under_test.process_audio(_audio_buffer, _audio_buffer);\n    recv_event = _event_output.pop();\n    EXPECT_EQ(RtEventType::NOTE_OFF, recv_event.type());\n    EXPECT_TRUE(_event_output.empty());\n}"
  },
  {
    "path": "test/unittests/plugins/external_plugins_test.cpp",
    "content": "#include <algorithm>\n#include <memory>\n#include <random>\n#include <tuple>\n\n#include \"gtest/gtest.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/host_control_mockup.h\"\n#include \"library/internal_plugin.h\"\n#include \"library/plugin_registry.h\"\n\n#include \"plugins/brickworks/bitcrusher_plugin.h\"\n#include \"plugins/brickworks/compressor_plugin.cpp\"\n#include \"plugins/brickworks/cab_sim_plugin.cpp\"\n#include \"plugins/brickworks/ring_mod_plugin.cpp\"\n#include \"plugins/brickworks/bitcrusher_plugin.cpp\"\n#include \"plugins/brickworks/wah_plugin.cpp\"\n#include \"plugins/brickworks/eq3band_plugin.cpp\"\n#include \"plugins/brickworks/phaser_plugin.cpp\"\n#include \"plugins/brickworks/chorus_plugin.cpp\"\n#include \"plugins/brickworks/vibrato_plugin.cpp\"\n#include \"plugins/brickworks/combdelay_plugin.cpp\"\n#include \"plugins/brickworks/flanger_plugin.cpp\"\n#include \"plugins/brickworks/saturation_plugin.cpp\"\n#include \"plugins/brickworks/noise_gate_plugin.cpp\"\n#include \"plugins/brickworks/tremolo_plugin.cpp\"\n#include \"plugins/brickworks/notch_plugin.cpp\"\n#include \"plugins/brickworks/multi_filter_plugin.cpp\"\n#include \"plugins/brickworks/highpass_plugin.cpp\"\n#include \"plugins/brickworks/clip_plugin.cpp\"\n#include \"plugins/brickworks/fuzz_plugin.cpp\"\n#include \"plugins/brickworks/dist_plugin.cpp\"\n#include \"plugins/brickworks/drive_plugin.cpp\"\n#include \"plugins/freeverb_plugin.cpp\"\n\nnamespace sushi::internal::bitcrusher_plugin\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(BitcrusherPlugin& plugin) : _plugin(plugin) {}\n\n    [[nodiscard]] FloatParameterValue* samplerate_ratio()\n    {\n        return _plugin._samplerate_ratio;\n    }\n\n    [[nodiscard]] IntParameterValue* bit_depth()\n    {\n        return _plugin._bit_depth;\n    }\n\nprivate:\n    BitcrusherPlugin& _plugin;\n};\n\n} // end sushi::internal::bitcrusher_plugin\n\nusing namespace sushi;\n\nconstexpr float TEST_SAMPLERATE = 48000;\nconstexpr int   TEST_CHANNEL_COUNT = 2;\n\nconstexpr int TEST_PROCESS_N_ITERATIONS = 128;\n\n\n\n/**\n * @brief Helper function to instantiate internal plugins from a UID\n *        We need to return everything because we can't use google test macros\n *        inside a non-void-returning function\n */\nstd::tuple<ProcessorReturnCode, ProcessorReturnCode, std::shared_ptr<Processor>> instantiate_plugin(const std::string& uid)\n{\n    HostControlMockup host_control;\n    PluginRegistry registry;\n\n    PluginInfo pinfo = { .uid = uid, .path= \"\", .type = PluginType::INTERNAL};\n\n    auto hc = host_control.make_host_control_mockup(TEST_SAMPLERATE);\n    auto [processor_status, plugin] = registry.new_instance(pinfo, hc, TEST_SAMPLERATE);\n\n    ProcessorReturnCode init_status = plugin->init(TEST_SAMPLERATE);\n    plugin->set_enabled(true);\n    plugin->set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n\n    return std::make_tuple(processor_status, init_status, plugin);\n}\n\n/**\n * @brief Set a random value for all the parameters of a plugin\n *\n * @param plugin Plugin object\n * @param dev Random number generator to use\n * @param dist Statistical distribution\n */\ntemplate<class random_device, class random_dist>\nvoid set_plugin_random_parameters(Processor* plugin,\n                                  random_device& dev, random_dist& dist)\n{\n    for (auto& pd : plugin->all_parameters())\n    {\n        plugin->process_event(RtEvent::make_parameter_change_event(plugin->id(), 0, pd->id(), dist(dev)));\n    }\n}\n\n/**\n * @brief Test that silence input produces silence output,\n *        over TEST_PROCESS_N_ITERATIONS while randomly varying parameters between each buffer\n */\nvoid test_fx_plugin_silencein_silenceout(const std::string& uid, float error_margin)\n{\n    std::ranlux24 rand_dev;\n    std::uniform_real_distribution<float> dist{0.0f, 1.0f};\n\n    auto [processor_status, init_status, plugin] = instantiate_plugin(uid);\n    ASSERT_EQ(processor_status, ProcessorReturnCode::OK);\n    ASSERT_EQ(init_status, ProcessorReturnCode::OK);\n\n    ChunkSampleBuffer in_buffer{TEST_CHANNEL_COUNT};\n    ChunkSampleBuffer out_buffer{TEST_CHANNEL_COUNT};\n\n    for (int i = 0; i < TEST_PROCESS_N_ITERATIONS; i++)\n    {\n        set_plugin_random_parameters(plugin.get(), rand_dev, dist);\n        plugin->process_audio(in_buffer, out_buffer);\n        test_utils::assert_buffer_value(0.0f, out_buffer, error_margin);\n    }\n}\n\n/**\n * @brief Test that white noise input does not generate NaNs,\n *        over TEST_PROCESS_N_ITERATIONS while randomly varying parameters between each buffer\n */\nvoid test_fx_plugin_noisein_notnan(const std::string& uid)\n{\n    std::ranlux24 rand_dev;\n    std::uniform_real_distribution<float> dist{0.0f, 1.0f};\n\n    auto [processor_status, init_status, plugin] = instantiate_plugin(uid);\n    ASSERT_EQ(processor_status, ProcessorReturnCode::OK);\n    ASSERT_EQ(init_status, ProcessorReturnCode::OK);\n\n    ChunkSampleBuffer in_buffer{TEST_CHANNEL_COUNT};\n    ChunkSampleBuffer out_buffer{TEST_CHANNEL_COUNT};\n\n    for (int i = 0; i < TEST_PROCESS_N_ITERATIONS; i++)\n    {\n        set_plugin_random_parameters(plugin.get(), rand_dev, dist);\n        plugin->process_audio(in_buffer, out_buffer);\n        test_utils::assert_buffer_not_nan(out_buffer);\n    }\n}\n\n/**\n * @brief Test that plugin does not write outside its output buffer boundary,\n *        by feeding it white noise over TEST_PROCESS_N_ITERATIONS \n *        while randomly varying parameters between each buffer\n */\nvoid test_fx_plugin_buffers_not_overflow(const std::string& uid)\n{\n    std::ranlux24 rand_dev;\n    std::uniform_real_distribution<float> dist{0.0f, 1.0f};\n\n    auto [processor_status, init_status, plugin] = instantiate_plugin(uid);\n    ASSERT_EQ(processor_status, ProcessorReturnCode::OK);\n    ASSERT_EQ(init_status, ProcessorReturnCode::OK);\n\n    ChunkSampleBuffer in_buffer{TEST_CHANNEL_COUNT+1};\n    ChunkSampleBuffer out_buffer{TEST_CHANNEL_COUNT+1};\n\n    for (int i = 0; i < TEST_PROCESS_N_ITERATIONS; i++)\n    {\n        set_plugin_random_parameters(plugin.get(), rand_dev, dist);\n        plugin->process_audio(in_buffer, out_buffer);\n        test_utils::assert_buffer_value(0.0f, SampleBuffer<AUDIO_CHUNK_SIZE>::create_non_owning_buffer(out_buffer, 2, 1));\n    }\n}\n\n/**\n * @brief Run a series of test on a given plugin class:\n *              - instantiation w. label & UID check\n *              - channel count\n *              - silence input produces silence output, while randomly setting parameters\n *              - white noise input does not produce NaNs, while randomly setting parameters\n */\nvoid test_fx_plugin_instantiation(const std::string& uid,\n                                  const std::string& label)\n{\n\n    auto [processor_status, init_status, plugin] = instantiate_plugin(uid);\n    ASSERT_EQ(processor_status, ProcessorReturnCode::OK);\n    ASSERT_EQ(init_status, ProcessorReturnCode::OK);\n    ASSERT_EQ(uid, plugin->name());\n    ASSERT_EQ(plugin->label(), label);\n    ASSERT_EQ(TEST_CHANNEL_COUNT, plugin->output_channels());\n    ASSERT_EQ(TEST_CHANNEL_COUNT, plugin->input_channels());\n}\n\n// We could have packed more test functions into less TEST cases,\n// but then it's hard to figure out what is failing.\n// Use a macro to automatically expand one-line into the four test cases\n\n#define EXTERNAL_PLUGIN_TEST_CASES(testlabel, plugin_uid, plugin_label, error_margin)\\\n    TEST(TestExternalPlugins, testlabel##Instantiation)\\\n    {\\\n        test_fx_plugin_instantiation(plugin_uid, plugin_label);\\\n    }\\\n    TEST(TestExternalPlugins, testlabel##SilenceInSilenceOut)\\\n    {\\\n        test_fx_plugin_silencein_silenceout(plugin_uid, error_margin);\\\n    }\\\n    TEST(TestExternalPlugins, testlabel##NoiseInNotNan)\\\n    {\\\n        test_fx_plugin_noisein_notnan(plugin_uid);\\\n    }\\\n    TEST(TestExternalPlugins, testlabel##BuffersDontOverflow)\\\n    {\\\n        test_fx_plugin_buffers_not_overflow(plugin_uid);\\\n    }\n\nEXTERNAL_PLUGIN_TEST_CASES(CabSim, \"sushi.brickworks.cab_sim\", \"Cab Simulator\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(Chorus, \"sushi.brickworks.chorus\", \"Chorus\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(Clip, \"sushi.brickworks.clip\", \"Clip\", 1.0e-2f)\nEXTERNAL_PLUGIN_TEST_CASES(CombDelay,\"sushi.brickworks.comb_delay\", \"Comb Delay\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(Compressor, \"sushi.brickworks.compressor\", \"Compressor\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(Dist, \"sushi.brickworks.dist\", \"Distortion\", 1.0e-2f)\nEXTERNAL_PLUGIN_TEST_CASES(Drive, \"sushi.brickworks.drive\", \"Drive\", 1.0e-2f)\nEXTERNAL_PLUGIN_TEST_CASES(Eq3band, \"sushi.brickworks.eq3band\", \"3-band Equalizer\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(Flanger, \"sushi.brickworks.flanger\", \"Flanger\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(Fuzz, \"sushi.brickworks.fuzz\", \"Fuzz\", 1.0e-2f)\nEXTERNAL_PLUGIN_TEST_CASES(HighPass, \"sushi.brickworks.highpass\", \"HighPass\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(MultiFilter, \"sushi.brickworks.multi_filter\", \"MultiFilter\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(Notch, \"sushi.brickworks.notch\", \"Notch\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(Phaser, \"sushi.brickworks.phaser\", \"Phaser\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(RingMod, \"sushi.brickworks.ring_mod\", \"Ring Modulator\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(Saturation, \"sushi.brickworks.saturation\", \"Saturation\", 1.0e-2f)\nEXTERNAL_PLUGIN_TEST_CASES(Tremolo, \"sushi.brickworks.tremolo\", \"Tremolo\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(Vibrato, \"sushi.brickworks.vibrato\", \"Vibrato\", 1.0e-4f)\nEXTERNAL_PLUGIN_TEST_CASES(Wah, \"sushi.brickworks.wah\", \"Wah\", 1.0e-4f)\n\nEXTERNAL_PLUGIN_TEST_CASES(Freeverb, \"sushi.testing.freeverb\", \"Freeverb\", 1.0e-4f)\n\n\n// the SilenceIn test is tricky for the NoiseGate atm, so we skip it\n\nTEST(TestExternalPlugins, TestNoiseGateInstantiation)\n{\n    test_fx_plugin_instantiation(\"sushi.brickworks.noise_gate\", \"Noise gate\");\n}\n\nTEST(TestExternalPlugins, TestNoiseGateNoiseInNotNaN)\n{\n    test_fx_plugin_noisein_notnan(\"sushi.brickworks.noise_gate\");\n}\n\nTEST(TestExternalPlugins, TestNoiseGateBuffersDontOverflow)\n{\n    test_fx_plugin_buffers_not_overflow(\"sushi.brickworks.noise_gate\");\n}\n\n\n// Bitcrusher plugin is an exception because it has one integer parameter,\n// and the silence threshold heavily depends on the bit depth parameter\n\nclass TestBitcrusherPlugin : public ::testing::Test\n{\nprotected:\n    TestBitcrusherPlugin()\n    {\n        _rand_gen.seed(1234);\n    }\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<bitcrusher_plugin::BitcrusherPlugin>(_host_control.make_host_control_mockup());\n\n        _accessor = std::make_unique<sushi::internal::bitcrusher_plugin::Accessor>(*_module_under_test);\n\n        ProcessorReturnCode status = _module_under_test->init(TEST_SAMPLERATE);\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n    }\n\n    void SetRandomParameters()\n    {\n        _accessor->samplerate_ratio()->set_processed(_sr_dist(_rand_gen));\n        _accessor->bit_depth()->set_processed(static_cast<float>(_bd_dist(_rand_gen)));\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<bitcrusher_plugin::BitcrusherPlugin> _module_under_test;\n\n    std::unique_ptr<sushi::internal::bitcrusher_plugin::Accessor> _accessor;\n\n    std::ranlux24 _rand_gen;\n    std::uniform_real_distribution<float> _sr_dist{0.0f, 1.0f};\n    std::uniform_int_distribution<int> _bd_dist{1, 16};\n};\n\nTEST_F(TestBitcrusherPlugin, TestInstantiation)\n{\n    ASSERT_TRUE(_module_under_test.get());\n    ASSERT_EQ(\"Bitcrusher\", _module_under_test->label());\n    ASSERT_EQ(\"sushi.brickworks.bitcrusher\", _module_under_test->name());\n    ASSERT_EQ(bitcrusher_plugin::BitcrusherPlugin::static_uid(), _module_under_test->uid());\n}\n\nTEST_F(TestBitcrusherPlugin, TestSilenceInSilenceOut)\n{\n    ChunkSampleBuffer in_buffer{TEST_CHANNEL_COUNT};\n    ChunkSampleBuffer out_buffer{TEST_CHANNEL_COUNT};\n\n    for (int i = 0; i < TEST_PROCESS_N_ITERATIONS; i++)\n    {\n        SetRandomParameters();\n        _module_under_test->process_audio(in_buffer, out_buffer);\n        // Threshold is dependent on bitdepth resolution\n        test_utils::assert_buffer_value(0.0f, out_buffer, 1.0f / (1.0f + static_cast<float>(_accessor->bit_depth()->processed_value())));\n    }\n}\n\nTEST_F(TestBitcrusherPlugin, TestNoiseInputNotNan)\n{\n    ChunkSampleBuffer in_buffer{TEST_CHANNEL_COUNT};\n    ChunkSampleBuffer out_buffer{TEST_CHANNEL_COUNT};\n\n    for (int i = 0; i < TEST_PROCESS_N_ITERATIONS; i++)\n    {\n        test_utils::fill_buffer_with_noise(in_buffer);\n        SetRandomParameters();\n        _module_under_test->process_audio(in_buffer, out_buffer);\n        test_utils::assert_buffer_not_nan(out_buffer);\n    }\n}\n\n"
  },
  {
    "path": "test/unittests/plugins/plugins_test.cpp",
    "content": "#include <algorithm>\n#include <memory>\n\n#include \"gtest/gtest.h\"\n\n#include \"test_utils/plugin_accessors.h\"\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/host_control_mockup.h\"\n\n#include \"library/internal_plugin.h\"\n#include \"plugins/passthrough_plugin.cpp\"\n#include \"plugins/gain_plugin.cpp\"\n#include \"plugins/lfo_plugin.cpp\"\n#include \"plugins/equalizer_plugin.cpp\"\n#include \"plugins/peak_meter_plugin.cpp\"\n#include \"plugins/wav_writer_plugin.cpp\"\n#include \"plugins/mono_summing_plugin.cpp\"\n#include \"plugins/sample_delay_plugin.cpp\"\n#include \"plugins/stereo_mixer_plugin.cpp\"\n#include \"dsp_library/biquad_filter.cpp\"\n\nusing namespace sushi;\n\nconstexpr float TEST_SAMPLERATE = 48000;\nconstexpr int   TEST_CHANNEL_COUNT = 2;\nstatic const std::string WRITE_FILE = \"write_test\";\n\nclass TestPassthroughPlugin : public ::testing::Test\n{\nprotected:\n    TestPassthroughPlugin() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<passthrough_plugin::PassthroughPlugin>(_host_control.make_host_control_mockup(TEST_SAMPLERATE));\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<InternalPlugin> _module_under_test;\n};\n\nTEST_F(TestPassthroughPlugin, TestInstantiation)\n{\n    ASSERT_TRUE(_module_under_test.get());\n}\n\nTEST_F(TestPassthroughPlugin, TestInitialization)\n{\n    _module_under_test->init(48000);\n    ASSERT_TRUE(_module_under_test.get());\n    ASSERT_EQ(\"Passthrough\", _module_under_test->label());\n    ASSERT_EQ(\"sushi.testing.passthrough\", _module_under_test->name());\n}\n\n// Fill a buffer with ones and test that they are passed through unchanged\nTEST_F(TestPassthroughPlugin, TestProcess)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(TEST_CHANNEL_COUNT);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(TEST_CHANNEL_COUNT);\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n    RtSafeRtEventFifo event_queue;\n    ASSERT_TRUE(event_queue.empty());\n    _module_under_test->set_event_output(&event_queue);\n    RtEvent event = RtEvent::make_note_on_event(0, 0, 0, 0, 0);\n\n    _module_under_test->process_event(event);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(1.0, out_buffer);\n    ASSERT_FALSE(event_queue.empty());\n}\n\nclass TestGainPlugin : public ::testing::Test\n{\nprotected:\n    TestGainPlugin() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<gain_plugin::GainPlugin>(_host_control.make_host_control_mockup());\n\n        _accessor = std::make_unique<sushi::internal::gain_plugin::Accessor>(*_module_under_test);\n\n        ProcessorReturnCode status = _module_under_test->init(TEST_SAMPLERATE);\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<gain_plugin::GainPlugin> _module_under_test;\n\n    std::unique_ptr<sushi::internal::gain_plugin::Accessor> _accessor;\n};\n\nTEST_F(TestGainPlugin, TestInstantiation)\n{\n    ASSERT_TRUE(_module_under_test.get());\n    ASSERT_EQ(\"Gain\", _module_under_test->label());\n    ASSERT_EQ(\"sushi.testing.gain\", _module_under_test->name());\n    ASSERT_EQ(gain_plugin::GainPlugin::static_uid(), _module_under_test->uid());\n}\n\nTEST_F(TestGainPlugin, TestChannelSetup)\n{\n    ASSERT_EQ(2, _module_under_test->output_channels());\n    ASSERT_EQ(2, _module_under_test->input_channels());\n\n    _module_under_test->set_channels(1, 1);\n    ASSERT_EQ(1, _module_under_test->output_channels());\n    ASSERT_EQ(1, _module_under_test->input_channels());\n}\n\n// Fill a buffer with ones, set gain to 2 and process it\nTEST_F(TestGainPlugin, TestProcess)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(2);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(2);\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n\n    _accessor->gain_parameter()->set(0.875f);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(2.0f, out_buffer, test_utils::DECIBEL_ERROR);\n}\n\nclass TestEqualizerPlugin : public ::testing::Test\n{\nprotected:\n    TestEqualizerPlugin() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<equalizer_plugin::EqualizerPlugin>(_host_control.make_host_control_mockup(TEST_SAMPLERATE));\n\n        _accessor = std::make_unique<sushi::internal::equalizer_plugin::Accessor>(_module_under_test.get());\n\n        ProcessorReturnCode status = _module_under_test->init(TEST_SAMPLERATE);\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<equalizer_plugin::EqualizerPlugin> _module_under_test;\n\n    std::unique_ptr<sushi::internal::equalizer_plugin::Accessor> _accessor;\n};\n\nTEST_F(TestEqualizerPlugin, TestInstantiation)\n{\n    ASSERT_TRUE(_module_under_test.get());\n    ASSERT_EQ(\"Equalizer\", _module_under_test->label());\n    ASSERT_EQ(\"sushi.testing.equalizer\", _module_under_test->name());\n}\n\nTEST_F(TestEqualizerPlugin, TestChannelSetup)\n{\n    ASSERT_EQ(2, _module_under_test->output_channels());\n    ASSERT_EQ(2, _module_under_test->input_channels());\n\n    _module_under_test->set_channels(1, 1);\n    ASSERT_EQ(1, _module_under_test->output_channels());\n    ASSERT_EQ(1, _module_under_test->input_channels());\n}\n\n// Test silence in -> silence out\nTEST_F(TestEqualizerPlugin, TestProcess)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(2);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(2);\n    test_utils::fill_sample_buffer(in_buffer, 0.0f);\n\n    // Get the registered parameters, check they exist and call set on them.\n    auto freq_param = static_cast<const FloatParameterDescriptor*>(_module_under_test->parameter_from_name(\"frequency\"));\n    ASSERT_TRUE(freq_param);\n\n    auto gain_param = static_cast<const FloatParameterDescriptor*>(_module_under_test->parameter_from_name(\"gain\"));\n    ASSERT_TRUE(gain_param);\n\n    auto q_param = static_cast<const FloatParameterDescriptor*>(_module_under_test->parameter_from_name(\"q\"));\n    ASSERT_TRUE(q_param);\n\n    _accessor->frequency()->set(0.1991991991991992f);\n    _accessor->gain()->set(0.625f);\n    _accessor->q()->set(0.1f);\n\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(0.0f, out_buffer);\n}\n\nclass TestPeakMeterPlugin : public ::testing::Test\n{\nprotected:\n    TestPeakMeterPlugin() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<peak_meter_plugin::PeakMeterPlugin>(_host_control.make_host_control_mockup(TEST_SAMPLERATE));\n        ProcessorReturnCode status = _module_under_test->init(TEST_SAMPLERATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n        _module_under_test->set_event_output(&_fifo);\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<peak_meter_plugin::PeakMeterPlugin> _module_under_test;\n    RtSafeRtEventFifo _fifo;\n};\n\nTEST_F(TestPeakMeterPlugin, TestInstantiation)\n{\n    ASSERT_TRUE(_module_under_test.get());\n    ASSERT_EQ(\"Peak Meter\", _module_under_test->label());\n    ASSERT_EQ(\"sushi.testing.peakmeter\", _module_under_test->name());\n}\n\nTEST_F(TestPeakMeterPlugin, TestProcess)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(TEST_CHANNEL_COUNT);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(TEST_CHANNEL_COUNT);\n    test_utils::fill_sample_buffer(in_buffer, 0.5f);\n\n    /* Process enough samples to catch some event outputs */\n    int no_of_process_calls = static_cast<int>(TEST_SAMPLERATE / (peak_meter_plugin::DEFAULT_REFRESH_RATE * AUDIO_CHUNK_SIZE));\n    ASSERT_TRUE(_fifo.empty());\n    for (int i = 0; i <= no_of_process_calls ; ++i)\n    {\n        _module_under_test->process_audio(in_buffer, out_buffer);\n    }\n    /* check that audio goes through unprocessed */\n    test_utils::assert_buffer_value(0.5f, out_buffer);\n\n    RtEvent event;\n    ASSERT_TRUE(_fifo.pop(event));\n    EXPECT_EQ(RtEventType::FLOAT_PARAMETER_CHANGE, event.type());\n    EXPECT_EQ(_module_under_test->id(), event.processor_id());\n    /*  The rms and dB calculations are tested separately, just test that it is a reasonable value */\n    EXPECT_GT(event.parameter_change_event()->value(), 0.5f);\n\n    /* Set the rate parameter to minimum */\n    auto rate_id = _module_under_test->parameter_from_name(\"update_rate\")->id();\n    _module_under_test->process_event(RtEvent::make_parameter_change_event(_module_under_test->id(),\n                                                                           0, rate_id, 0.0f));\n    while (_fifo.pop(event)) {}\n\n    ASSERT_TRUE(_fifo.empty());\n    for (int i = 0; i <= no_of_process_calls * 5 ; ++i)\n    {\n        _module_under_test->process_audio(in_buffer, out_buffer);\n    }\n    ASSERT_TRUE(_fifo.empty());\n}\n\nTEST_F(TestPeakMeterPlugin, TestClipDetection)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(TEST_CHANNEL_COUNT);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(TEST_CHANNEL_COUNT);\n    auto first_channel = ChunkSampleBuffer::create_non_owning_buffer(in_buffer, 0, 1);\n    test_utils::fill_sample_buffer(in_buffer, 0.5f);\n    test_utils::fill_sample_buffer(first_channel, 1.5f);\n\n    auto clip_ch_0_id = _module_under_test->parameter_from_name(\"clip_0\")->id();\n    auto clip_ch_1_id = _module_under_test->parameter_from_name(\"clip_1\")->id();\n\n    EXPECT_FLOAT_EQ(0.0f,_module_under_test->parameter_value(clip_ch_0_id).second);\n    EXPECT_FLOAT_EQ(0.0f,_module_under_test->parameter_value(clip_ch_1_id).second);\n\n    /* Run once and check that the parameter value has changed for the left channel*/\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    EXPECT_FLOAT_EQ(1.0f,_module_under_test->parameter_value(clip_ch_0_id).second);\n    EXPECT_FLOAT_EQ(0.0f,_module_under_test->parameter_value(clip_ch_1_id).second);\n\n    /* Lower volume and run until the hold time has passed */\n    test_utils::fill_sample_buffer(in_buffer, 0.5f);\n    for (int i = 0; i <= TEST_SAMPLERATE * 6 / AUDIO_CHUNK_SIZE ; ++i)\n    {\n        _module_under_test->process_audio(in_buffer, out_buffer);\n    }\n\n    EXPECT_FLOAT_EQ(0.0f,_module_under_test->parameter_value(clip_ch_0_id).second);\n    EXPECT_FLOAT_EQ(0.0f,_module_under_test->parameter_value(clip_ch_1_id).second);\n\n    /* Pop the first event and verify it was a clip parameter change */\n    RtEvent event;\n    ASSERT_TRUE(_fifo.pop(event));\n    EXPECT_EQ(RtEventType::FLOAT_PARAMETER_CHANGE, event.type());\n    EXPECT_EQ(clip_ch_0_id, event.parameter_change_event()->param_id());\n\n    /* Test with linked channels */\n    test_utils::fill_sample_buffer(first_channel, 1.5f);\n    event = RtEvent::make_parameter_change_event(0,0,_module_under_test->parameter_from_name(\"link_channels\")->id(), 1.0f);\n    _module_under_test->process_event(event);\n    /* Run once and check that the parameter value has changed for both channels */\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    EXPECT_FLOAT_EQ(1.0f,_module_under_test->parameter_value(clip_ch_0_id).second);\n    EXPECT_FLOAT_EQ(1.0f,_module_under_test->parameter_value(clip_ch_1_id).second);\n}\n\nTEST(TestPeakMeterPluginInternal, TestTodBConversion)\n{\n    EXPECT_FLOAT_EQ(0.0f, peak_meter_plugin::to_normalised_dB(0.0f));         // minimum\n    EXPECT_NEAR(0.5f, peak_meter_plugin::to_normalised_dB(0.003981f), 0.0001f); // -48 dB\n    EXPECT_NEAR(0.8333f, peak_meter_plugin::to_normalised_dB(1.0f), 0.0001f);  //  0 dB\n    EXPECT_FLOAT_EQ(1.0f, peak_meter_plugin::to_normalised_dB(15.9f));        // +24 dB\n    EXPECT_FLOAT_EQ(1.0f, peak_meter_plugin::to_normalised_dB(251.2f));       // +48 dB (clamped)\n}\n\nclass TestLfoPlugin : public ::testing::Test\n{\nprotected:\n    TestLfoPlugin() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<lfo_plugin::LfoPlugin>(_host_control.make_host_control_mockup());\n        ProcessorReturnCode status = _module_under_test->init(TEST_SAMPLERATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n        _module_under_test->set_event_output(&_queue);\n        _module_under_test->set_enabled(true);\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<lfo_plugin::LfoPlugin> _module_under_test;\n    RtSafeRtEventFifo _queue;\n};\n\nTEST_F(TestLfoPlugin, TestInstantiation)\n{\n    ASSERT_TRUE(_module_under_test.get());\n    ASSERT_EQ(\"Lfo\", _module_under_test->label());\n    ASSERT_EQ(\"sushi.testing.lfo\", _module_under_test->name());\n}\n\nTEST_F(TestLfoPlugin, TestProcess)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(0);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(0);\n    // Calling process should result in a parameter update event.\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    ASSERT_FALSE(_queue.empty());\n    RtEvent event;\n    ASSERT_TRUE(_queue.pop(event));\n    EXPECT_EQ(RtEventType::FLOAT_PARAMETER_CHANGE, event.type());\n\n    // Connect a cv output to it\n    auto param = _module_under_test->parameter_from_name(\"out\");\n    _module_under_test->connect_cv_from_parameter(param->id(), 2);\n    // Calling process should now result in a cv event instead.\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    ASSERT_FALSE(_queue.empty());\n    ASSERT_TRUE(_queue.pop(event));\n    EXPECT_EQ(RtEventType::CV_EVENT, event.type());\n    EXPECT_EQ(2, event.cv_event()->cv_id());\n}\n\nclass TestWavWriterPlugin : public ::testing::Test\n{\nprotected:\n    TestWavWriterPlugin() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<wav_writer_plugin::WavWriterPlugin>(_host_control.make_host_control_mockup(TEST_SAMPLERATE));\n\n        _accessor = std::make_unique<sushi::internal::wav_writer_plugin::Accessor>(*_module_under_test);\n\n        auto status = _module_under_test->init(TEST_SAMPLERATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n        _module_under_test->set_event_output(&_fifo);\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_channels(wav_writer_plugin::N_AUDIO_CHANNELS, wav_writer_plugin::N_AUDIO_CHANNELS);\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<wav_writer_plugin::WavWriterPlugin> _module_under_test;\n\n    std::unique_ptr<sushi::internal::wav_writer_plugin::Accessor> _accessor;\n\n    RtEventFifo<10> _fifo;\n};\n\nTEST_F(TestWavWriterPlugin, TestInitialization)\n{\n    ASSERT_TRUE(_module_under_test.get());\n    ASSERT_EQ(\"Wav writer\", _module_under_test->label());\n    ASSERT_EQ(\"sushi.testing.wav_writer\", _module_under_test->name());\n}\n\n// Fill a buffer with ones and test that they are passed through unchanged\nTEST_F(TestWavWriterPlugin, TestProcess)\n{\n    ObjectId record_param_id = _module_under_test->parameter_from_name(\"recording\")->id();\n    ObjectId file_property_id = _module_under_test->parameter_from_name(\"destination_file\")->id();\n\n    // Set up buffers and events\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(wav_writer_plugin::N_AUDIO_CHANNELS);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(wav_writer_plugin::N_AUDIO_CHANNELS);\n    test_utils::fill_sample_buffer(in_buffer, 1.0f);\n    std::string path = \"./\";\n    path.append(WRITE_FILE);\n    RtEvent start_recording_event = RtEvent::make_parameter_change_event(0, 0, record_param_id, 1.0f);\n    RtEvent stop_recording_event = RtEvent::make_parameter_change_event(0, 0, record_param_id, 0.0f);\n\n    // Test setting path property\n    _module_under_test->set_property_value(file_property_id, path);\n\n    // Test start recording and open file\n    _module_under_test->process_event(start_recording_event);\n    ASSERT_TRUE(_accessor->recording_parameter()->domain_value());\n    ASSERT_EQ(wav_writer_plugin::WavWriterStatus::SUCCESS, _accessor->start_recording());\n\n    // Test processing\n    _accessor->recording_parameter()->set_values(true, true);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(1.0f, in_buffer);\n    test_utils::assert_buffer_value(1.0f, out_buffer);\n\n    // Test Writing.\n    _accessor->recording_parameter()->set_values(false, false); // set recording to false - to write immediately.\n    ASSERT_EQ(_module_under_test->input_channels() * AUDIO_CHUNK_SIZE, _accessor->write_to_file());\n\n    // Test end recording and close file\n    _module_under_test->process_event(stop_recording_event);\n    ASSERT_FALSE(_accessor->recording_parameter()->domain_value());\n    ASSERT_EQ(wav_writer_plugin::WavWriterStatus::SUCCESS, _accessor->stop_recording());\n\n    // Verify written samples\n    path.append(\".wav\");\n    SF_INFO soundfile_info;\n    memset(&soundfile_info, 0, sizeof(SF_INFO));\n    SNDFILE* file = sf_open(path.c_str(), SFM_READ, &soundfile_info);\n    if (sf_error(file))\n    {\n        FAIL() << \"While opening file \" << path.c_str() << \" \" << sf_strerror(file) << std::endl;\n    }\n    int number_of_samples = AUDIO_CHUNK_SIZE * _module_under_test->input_channels();\n\n    std::vector<float> written_data(number_of_samples);\n\n    ASSERT_EQ(AUDIO_CHUNK_SIZE, sf_readf_float(file, written_data.data(), AUDIO_CHUNK_SIZE));\n    for (int sample = 0; sample < number_of_samples; ++sample)\n    {\n        ASSERT_FLOAT_EQ(1.0f, written_data[sample]);\n    }\n    sf_close(file);\n    remove(path.c_str());\n}\n\nclass TestMonoSummingPlugin : public ::testing::Test\n{\nprotected:\n    TestMonoSummingPlugin() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<mono_summing_plugin::MonoSummingPlugin>(_host_control.make_host_control_mockup(TEST_SAMPLERATE));\n        auto status = _module_under_test->init(TEST_SAMPLERATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_event_output(&_fifo);\n        _module_under_test->set_channels(TEST_CHANNEL_COUNT, TEST_CHANNEL_COUNT);\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<mono_summing_plugin::MonoSummingPlugin> _module_under_test;\n    RtEventFifo<10> _fifo;\n};\n\nTEST_F(TestMonoSummingPlugin, TestInitialization)\n{\n    ASSERT_TRUE(_module_under_test.get());\n    ASSERT_EQ(\"Mono summing\", _module_under_test->label());\n    ASSERT_EQ(\"sushi.testing.mono_summing\", _module_under_test->name());\n}\n\n// Fill a buffer with ones and test that they are passed through unchanged\nTEST_F(TestMonoSummingPlugin, TestProcess)\n{\n    // Set up buffers and events\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(TEST_CHANNEL_COUNT);\n    for (int sample = 0; sample < AUDIO_CHUNK_SIZE; ++sample)\n    {\n        in_buffer.channel(0)[sample] = 1.0f;\n    }\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(TEST_CHANNEL_COUNT);\n\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    for (int sample = 0; sample < AUDIO_CHUNK_SIZE; ++sample)\n    {\n        ASSERT_FLOAT_EQ(1.0f, in_buffer.channel(0)[sample]);\n        ASSERT_FLOAT_EQ(0.0f, in_buffer.channel(1)[sample]);\n    }\n    test_utils::assert_buffer_value(1.0f, out_buffer);\n}\n\nclass TestSampleDelayPlugin : public ::testing::Test\n{\nprotected:\n    TestSampleDelayPlugin() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<sample_delay_plugin::SampleDelayPlugin>(_host_control.make_host_control_mockup(TEST_SAMPLERATE));\n        auto status = _module_under_test->init(TEST_SAMPLERATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_event_output(&_fifo);\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<sample_delay_plugin::SampleDelayPlugin> _module_under_test;\n    RtSafeRtEventFifo _fifo;\n};\n\nTEST_F(TestSampleDelayPlugin, TestInitialization)\n{\n    ASSERT_TRUE(_module_under_test.get());\n    ASSERT_EQ(\"Sample delay\", _module_under_test->label());\n    ASSERT_EQ(\"sushi.testing.sample_delay\", _module_under_test->name());\n}\n\n// Fill a buffer with ones and test that they are passed through unchanged\nTEST_F(TestSampleDelayPlugin, TestProcess)\n{\n    // Set up data\n    int n_audio_channels = TEST_CHANNEL_COUNT;\n    std::vector<int> delay_times = {0, 1, 5, 20, 62, 15, 2};\n    SampleBuffer<AUDIO_CHUNK_SIZE> zero_buffer(n_audio_channels);\n    SampleBuffer<AUDIO_CHUNK_SIZE> result_buffer(n_audio_channels);\n    SampleBuffer<AUDIO_CHUNK_SIZE> impulse_buffer(n_audio_channels);\n    for (int channel = 0; channel < n_audio_channels; channel++)\n    {\n        impulse_buffer.channel(channel)[0] = 1.0;\n    }\n\n    // Test processing\n    for (auto delay_time : delay_times)\n    {\n        // Parameter change event\n        auto delay_time_event = RtEvent::make_parameter_change_event(0, 0, 0, static_cast<float>(delay_time) / 48000.0f);\n        _module_under_test->process_event(delay_time_event);\n\n        // Process audio\n        _module_under_test->process_audio(zero_buffer, result_buffer);\n        _module_under_test->process_audio(impulse_buffer, result_buffer);\n\n        // Check the impulse has been delayed the correct number of samples\n        for (int sample_idx = 0; sample_idx < AUDIO_CHUNK_SIZE; sample_idx++)\n        {\n            for (int channel = 0; channel < n_audio_channels; channel++)\n            {\n                if (sample_idx == delay_time)\n                {\n                    EXPECT_FLOAT_EQ(1.0f, result_buffer.channel(channel)[sample_idx]);\n                }\n                else\n                {\n                    EXPECT_FLOAT_EQ(0.0f, result_buffer.channel(channel)[sample_idx]);\n                }\n            }\n        }\n    }\n}\n\nconstexpr int TEST_CHANNELS_STEREO = 2;\n\nclass TestStereoMixerPlugin : public ::testing::Test\n{\nprotected:\n    TestStereoMixerPlugin() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<stereo_mixer_plugin::StereoMixerPlugin>(_host_control.make_host_control_mockup(TEST_SAMPLERATE));\n\n        _accessor = std::make_unique<sushi::internal::stereo_mixer_plugin::Accessor>(*_module_under_test);\n\n        auto status = _module_under_test->init(TEST_SAMPLERATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_event_output(&_fifo);\n    }\n\n    void WaitForStableParameters()\n    {\n        // Run one empty process call to update the smoothers to the current parameter values\n        ChunkSampleBuffer temp_in_buffer(TEST_CHANNELS_STEREO);\n        ChunkSampleBuffer temp_out_buffer(TEST_CHANNELS_STEREO);\n        temp_in_buffer.clear();\n        temp_out_buffer.clear();\n        _module_under_test->process_audio(temp_in_buffer, temp_out_buffer);\n\n        // Update smoothers until they are stationary\n        while (! (_accessor->ch1_left_gain_smoother().stationary() &&\n                  _accessor->ch1_right_gain_smoother().stationary() &&\n                  _accessor->ch2_left_gain_smoother().stationary() &&\n                  _accessor->ch2_right_gain_smoother().stationary()))\n        {\n            _accessor->ch1_left_gain_smoother().next_value();\n            _accessor->ch1_right_gain_smoother().next_value();\n            _accessor->ch2_left_gain_smoother().next_value();\n            _accessor->ch2_right_gain_smoother().next_value();\n        }\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<stereo_mixer_plugin::StereoMixerPlugin> _module_under_test;\n\n    std::unique_ptr<sushi::internal::stereo_mixer_plugin::Accessor> _accessor;\n\n    RtSafeRtEventFifo _fifo;\n};\n\nTEST_F(TestStereoMixerPlugin, TestInitialization)\n{\n    ASSERT_TRUE(_module_under_test.get());\n    ASSERT_EQ(\"Stereo Mixer\", _module_under_test->label());\n    ASSERT_EQ(\"sushi.testing.stereo_mixer\", _module_under_test->name());\n}\n\nTEST_F(TestStereoMixerPlugin, TestProcess)\n{\n    // Set up data\n    int n_audio_channels = TEST_CHANNELS_STEREO;\n    SampleBuffer<AUDIO_CHUNK_SIZE> input_buffer(n_audio_channels);\n    SampleBuffer<AUDIO_CHUNK_SIZE> output_buffer(n_audio_channels);\n    SampleBuffer<AUDIO_CHUNK_SIZE> expected_buffer(n_audio_channels);\n\n    std::fill(input_buffer.channel(0), input_buffer.channel(0) + AUDIO_CHUNK_SIZE, 1.0f);\n    std::fill(input_buffer.channel(1), input_buffer.channel(1) + AUDIO_CHUNK_SIZE, -2.0f);\n\n    // Default configuration\n\n    std::fill(expected_buffer.channel(0), expected_buffer.channel(0) + AUDIO_CHUNK_SIZE, 1.0f);\n    std::fill(expected_buffer.channel(1), expected_buffer.channel(1) + AUDIO_CHUNK_SIZE, -2.0f);\n\n    _module_under_test->process_audio(input_buffer, output_buffer);\n    test_utils::compare_buffers<AUDIO_CHUNK_SIZE>(output_buffer, expected_buffer, 2);\n\n    // Standard stereo throughput, right input channel inverted\n\n    _accessor->ch1_pan()->set(0.0f);\n    _accessor->ch1_gain()->set(0.791523611713336f);\n    _accessor->ch1_invert_phase()->set(0.0f);\n    _accessor->ch2_pan()->set(1.0f);\n    _accessor->ch2_gain()->set(0.6944444444444444f);\n    _accessor->ch2_invert_phase()->set(1.0f);\n\n    std::fill(expected_buffer.channel(0), expected_buffer.channel(0) + AUDIO_CHUNK_SIZE, 0.5f);\n    std::fill(expected_buffer.channel(1), expected_buffer.channel(1) + AUDIO_CHUNK_SIZE, 0.2f);\n\n    WaitForStableParameters();\n\n    _module_under_test->process_audio(input_buffer, output_buffer);\n    test_utils::compare_buffers<AUDIO_CHUNK_SIZE>(output_buffer, expected_buffer, 2);\n\n    // Inverted panning, left input channel inverted\n\n    _accessor->ch1_pan()->set(1.0f);\n    _accessor->ch1_gain()->set(0.8118191722242023f);\n    _accessor->ch1_invert_phase()->set(1.0f);\n    _accessor->ch2_pan()->set(0.0f);\n    _accessor->ch2_gain()->set(0.7607112853777309f);\n    _accessor->ch2_invert_phase()->set(0.0f);\n\n    std::fill(expected_buffer.channel(0), expected_buffer.channel(0) + AUDIO_CHUNK_SIZE, -0.6f);\n    std::fill(expected_buffer.channel(1), expected_buffer.channel(1) + AUDIO_CHUNK_SIZE, -0.7f);\n\n    WaitForStableParameters();\n\n    _module_under_test->process_audio(input_buffer, output_buffer);\n    test_utils::compare_buffers<AUDIO_CHUNK_SIZE>(output_buffer, expected_buffer, 2);\n\n    // Mono summing\n    _accessor->ch1_pan()->set(0.5f);\n    _accessor->ch1_gain()->set(0.8333333333333334f);\n    _accessor->ch1_invert_phase()->set(0.0f);\n    _accessor->ch2_pan()->set(0.5f);\n    _accessor->ch2_gain()->set(0.8333333333333334f);\n    _accessor->ch2_invert_phase()->set(0.0f);\n\n    std::fill(expected_buffer.channel(0), expected_buffer.channel(0) + AUDIO_CHUNK_SIZE, -0.707946f);\n    std::fill(expected_buffer.channel(1), expected_buffer.channel(1) + AUDIO_CHUNK_SIZE, -0.707946f);\n\n    WaitForStableParameters();\n\n    _module_under_test->process_audio(input_buffer, output_buffer);\n    test_utils::compare_buffers<AUDIO_CHUNK_SIZE>(output_buffer, expected_buffer, 2);\n\n    // Pan law test\n    _accessor->ch1_pan()->set(0.35f);\n    _accessor->ch1_gain()->set(0.8333333333333334f);\n    _accessor->ch1_invert_phase()->set(0.0f);\n    _accessor->ch2_pan()->set(0.9f);\n    _accessor->ch2_gain()->set(0.8333333333333334f);\n    _accessor->ch2_invert_phase()->set(0.0f);\n\n    std::fill(expected_buffer.channel(0), expected_buffer.channel(0) + AUDIO_CHUNK_SIZE, 0.7955587392184001f + -0.28317642241051433f);\n    std::fill(expected_buffer.channel(1), expected_buffer.channel(1) + AUDIO_CHUNK_SIZE, 0.49555873921840016f + -1.8831764224105143f);\n\n    WaitForStableParameters();\n\n    _module_under_test->process_audio(input_buffer, output_buffer);\n    test_utils::compare_buffers<AUDIO_CHUNK_SIZE>(output_buffer, expected_buffer, 2);\n}\n"
  },
  {
    "path": "test/unittests/plugins/sample_player_plugin_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/host_control_mockup.h\"\n#include \"library/rt_event_fifo.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"plugins/sample_player_voice.cpp\"\n#include \"plugins/sample_player_plugin.cpp\"\n\nnamespace sushi::internal::sample_player_plugin\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(SamplePlayerPlugin& plugin) : _plugin(plugin) {}\n\n    [[nodiscard]] float*  sample_buffer()\n    {\n        return _plugin._sample_buffer;\n    }\n\n    // Not const: it's modified in the test\n    [[nodiscard]] dsp::Sample& sample()\n    {\n        return _plugin._sample;\n    }\n\nprivate:\n    SamplePlayerPlugin& _plugin;\n};\n\n}\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::sample_player_plugin;\nusing namespace sample_player_voice;\n\nconstexpr float TEST_SAMPLERATE = 44100;\nconst float SAMPLE_DATA[] = {1.0f, 2.0f, 2.0f, 1.0f, 1.0f};\nconst int SAMPLE_DATA_LENGTH = sizeof(SAMPLE_DATA) / sizeof(float);\n\nstatic const std::string SAMPLE_FILE = \"Kawai-K11-GrPiano-C4_mono.wav\";\n\n\n/* Test the Voice class */\nclass TestSamplerVoice : public ::testing::Test\n{\nprotected:\n    void SetUp() override\n    {\n        _module_under_test.set_sample(&_sample);\n        _module_under_test.set_samplerate(TEST_SAMPLERATE);\n        _module_under_test.set_envelope(0,0,1,0);\n    }\n\n    dsp::Sample _sample{SAMPLE_DATA, SAMPLE_DATA_LENGTH};\n    Voice _module_under_test;\n};\n\nTEST_F(TestSamplerVoice, TestInitialConditions)\n{\n    EXPECT_FALSE(_module_under_test.active());\n    sushi::SampleBuffer<AUDIO_CHUNK_SIZE> buffer(1);\n    buffer.clear();\n    _module_under_test.render(buffer);\n    test_utils::assert_buffer_value(0.0f, buffer);\n}\n\nTEST_F(TestSamplerVoice, TestNoteOn)\n{\n    sushi::SampleBuffer<AUDIO_CHUNK_SIZE> buffer(1);\n    buffer.clear();\n\n    _module_under_test.note_on(60, 1.0f, 10);\n    _module_under_test.render(buffer);\n\n    float* buf = buffer.channel(0);\n    EXPECT_FLOAT_EQ(0.0f, buf[5]);\n    EXPECT_FLOAT_EQ(0.0f, buf[9]);\n    EXPECT_FLOAT_EQ(1.0f, buf[10]);\n    EXPECT_FLOAT_EQ(2.0f, buf[12]);\n    EXPECT_FLOAT_EQ(0.0f, buf[15]);\n}\n\n/* Test note on and note of during same audio chunk */\nTEST_F(TestSamplerVoice, TestNoteOff)\n{\n    sushi::SampleBuffer<AUDIO_CHUNK_SIZE> buffer(1);\n    buffer.clear();\n\n    _module_under_test.note_on(60, 1.0f, 0);\n    _module_under_test.note_off(1.0f, 4);\n    _module_under_test.render(buffer);\n\n    float* buf = buffer.channel(0);\n    EXPECT_FLOAT_EQ(1.0f, buf[0]);\n    EXPECT_FLOAT_EQ(2.0f, buf[1]);\n    EXPECT_FLOAT_EQ(2.0f, buf[2]);\n    EXPECT_FLOAT_EQ(1.0f, buf[3]);\n    /* This is where the note should end */\n    EXPECT_FLOAT_EQ(0.0f, buf[4]);\n}\n\n\n/* Test the Plugin */\nclass TestSamplePlayerPlugin : public ::testing::Test\n{\nprotected:\n    TestSamplePlayerPlugin() = default;\n\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<SamplePlayerPlugin>(_host_control.make_host_control_mockup(TEST_SAMPLERATE));\n        ProcessorReturnCode status = _module_under_test->init(TEST_SAMPLERATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n        _accessor = std::make_unique<sushi::internal::sample_player_plugin::Accessor>(*_module_under_test);\n    }\n\n    void TearDown() override\n    {\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<SamplePlayerPlugin> _module_under_test;\n\n    std::unique_ptr<sushi::internal::sample_player_plugin::Accessor> _accessor;\n};\n\nTEST_F(TestSamplePlayerPlugin, TestSampleLoading)\n{\n    RtSafeRtEventFifo queue;\n    _module_under_test->set_event_output(&queue);\n    auto path = std::string(test_utils::get_data_dir_path());\n    path.append(SAMPLE_FILE);\n\n    ASSERT_EQ(nullptr, _accessor->sample_buffer());\n    auto status = _module_under_test->set_property_value(SAMPLE_PROPERTY_ID, path);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    // The plugin should have sent an event with the sample data to the dispatcher\n    auto event = _host_control._dummy_dispatcher.retrieve_event();\n    ASSERT_TRUE(event->maps_to_rt_event());\n    auto rt_event = event->to_rt_event(0);\n    ASSERT_EQ(RtEventType::DATA_PROPERTY_CHANGE, rt_event.type());\n\n    // Pass the RtEvent to the plugin manually\n    _module_under_test->process_event(rt_event);\n\n    // Sample should now be changed\n    ASSERT_NE(nullptr, _accessor->sample_buffer());\n\n    // Plugin should have put a delete event on the output queue, just check that it's there\n    ASSERT_FALSE(queue.empty());\n}\n\nTEST_F(TestSamplePlayerPlugin, TestProcessing)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(1);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(1);\n    _accessor->sample().set_sample(SAMPLE_DATA, SAMPLE_DATA_LENGTH);\n    out_buffer.clear();\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(0.0f, out_buffer);\n}\n\nTEST_F(TestSamplePlayerPlugin, TestEventProcessing)\n{\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(1);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(1);\n    BlobData data = load_sample_file(test_utils::get_data_dir_path().append(SAMPLE_FILE));\n    ASSERT_NE(0, data.size);\n    _accessor->sample().set_sample(reinterpret_cast<float*>(data.data), data.size * sizeof(float));\n    out_buffer.clear();\n    RtEvent note_on = RtEvent::make_note_on_event(0, 5, 0, 60, 1.0f);\n    RtEvent note_on2 = RtEvent::make_note_on_event(0, 50, 0, 65, 1.0f);\n    _module_under_test->process_event(note_on);\n    _module_under_test->process_event(note_on2);\n\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    // assert that something was written to the buffer\n    ASSERT_NE(0.0f, out_buffer.channel(0)[10]);\n    ASSERT_NE(0.0f, out_buffer.channel(0)[15]);\n\n    // Test that bypass works\n    _module_under_test->set_bypassed(true);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(0.0f, out_buffer);\n\n    // And that we have no hanging notes\n    _module_under_test->set_bypassed(false);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(0.0f, out_buffer);\n    delete[] data.data;\n}\n"
  },
  {
    "path": "test/unittests/plugins/send_return_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/host_control_mockup.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"plugins/send_return_factory.cpp\"\n#include \"plugins/send_plugin.cpp\"\n#include \"plugins/return_plugin.cpp\"\n\nnamespace sushi::internal::send_plugin\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(SendPlugin& plugin) : _plugin(plugin) {}\n\n    [[nodiscard]] return_plugin::ReturnPlugin* destination()\n    {\n        return _plugin._destination;\n    }\n\n    void set_destination(return_plugin::ReturnPlugin* destination)\n    {\n        _plugin._set_destination(destination);\n    }\n\nprivate:\n    SendPlugin& _plugin;\n};\n\n}\n\nnamespace sushi::internal::return_plugin\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(ReturnPlugin& plugin) : _plugin(plugin) {}\n\n    void swap_buffers()\n    {\n        _plugin._swap_buffers();\n    }\n\nprivate:\n    ReturnPlugin& _plugin;\n};\n\n}\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::send_plugin;\nusing namespace sushi::internal::return_plugin;\n\nconstexpr float TEST_SAMPLERATE = 44100;\n\nTEST(TestSendReturnFactory, TestFactoryCreation)\n{\n    SendReturnFactory factory;\n    HostControlMockup host_control_mockup;\n    auto host_ctrl = host_control_mockup.make_host_control_mockup(TEST_SAMPLERATE);\n    PluginInfo info{.uid = \"sushi.testing.send\", .path = \"\", .type = PluginType::INTERNAL};\n\n    auto [send_status, send_plugin] = factory.new_instance(info, host_ctrl, TEST_SAMPLERATE);\n    ASSERT_EQ(ProcessorReturnCode::OK, send_status);\n    ASSERT_EQ(\"Send\", send_plugin->label());\n    ASSERT_GT(send_plugin.use_count(), 0);\n    ASSERT_GT(send_plugin->id(), 0u);\n\n    info.uid = \"sushi.testing.return\";\n    auto [ret_status, return_plugin] = factory.new_instance(info, host_ctrl, TEST_SAMPLERATE);\n    ASSERT_EQ(ProcessorReturnCode::OK, ret_status);\n    ASSERT_EQ(\"Return\", return_plugin->label());\n    ASSERT_GT(return_plugin.use_count(), 0);\n    ASSERT_GT(return_plugin->id(), 0u);\n\n    // Negative test\n    info.uid = \"sushi.testing.aux_\";\n    auto [error_status, error_plugin] = factory.new_instance(info, host_ctrl, TEST_SAMPLERATE);\n    ASSERT_NE(ProcessorReturnCode::OK, error_status);\n    ASSERT_FALSE(error_plugin);\n}\n\n\nclass TestSendReturnPlugins : public ::testing::Test\n{\nprotected:\n    TestSendReturnPlugins() = default;\n\n    void SetUp() override\n    {\n        ASSERT_EQ(ProcessorReturnCode::OK, _send_instance.init(TEST_SAMPLERATE));\n        _send_instance.set_channels(2, 2);\n        _send_instance.set_active_rt_processing(true, 0);\n\n        ASSERT_EQ(ProcessorReturnCode::OK, _return_instance.init(TEST_SAMPLERATE));\n        _return_instance.set_channels(2, 2);\n        _return_instance.set_active_rt_processing(true, 1);\n    }\n\n    SendReturnFactory   _factory;\n    HostControlMockup   _host_control_mockup;\n    HostControl         _host_ctrl {_host_control_mockup.make_host_control_mockup(TEST_SAMPLERATE)};\n\n    SendPlugin          _send_instance {_host_ctrl, &_factory};\n    ReturnPlugin        _return_instance {_host_ctrl, &_factory};\n\n    sushi::internal::send_plugin::Accessor _send_accessor {_send_instance};\n    sushi::internal::return_plugin::Accessor _return_accessor {_return_instance};\n};\n\nTEST_F(TestSendReturnPlugins, TestDestinationSetting)\n{\n    PluginInfo info;\n    info.uid = \"sushi.testing.return\";\n    auto [status, return_instance_2] = _factory.new_instance(info, _host_ctrl, TEST_SAMPLERATE);\n    ASSERT_EQ(ProcessorReturnCode::OK, status);\n\n    _return_instance.set_name(\"return_1\");\n    return_instance_2->set_name(\"return_2\");\n\n    EXPECT_EQ(DEFAULT_DEST, _send_instance.property_value(DEST_PROPERTY_ID).second);\n    status = _send_instance.set_property_value(DEST_PROPERTY_ID, \"return_2\");\n    EXPECT_EQ(ProcessorReturnCode::OK, status);\n    EXPECT_EQ(_send_accessor.destination(), return_instance_2.get());\n    EXPECT_EQ(\"return_2\", _send_instance.property_value(DEST_PROPERTY_ID).second);\n\n    // Destroy the second return and it should be automatically unlinked.\n    return_instance_2.reset();\n    EXPECT_EQ(_send_accessor.destination(), nullptr);\n    EXPECT_EQ(DEFAULT_DEST, _send_instance.property_value(DEST_PROPERTY_ID).second);\n}\n\nTEST_F(TestSendReturnPlugins, TestProcessing)\n{\n    ChunkSampleBuffer buffer_1(2);\n    ChunkSampleBuffer buffer_2(2);\n    test_utils::fill_sample_buffer(buffer_1, 1.0f);\n\n    // Test that processing without destination doesn't break and passes though\n    _send_instance.process_audio(buffer_1, buffer_2);\n    test_utils::assert_buffer_value(1.0f, buffer_2);\n\n    _send_accessor.set_destination(&_return_instance);\n    _send_instance.process_audio(buffer_1, buffer_2);\n    buffer_2.clear();\n\n    // Swap manually and verify that signal is returned\n    _return_accessor.swap_buffers();\n    _return_instance.process_audio(buffer_1, buffer_2);\n    test_utils::assert_buffer_value(1.0f, buffer_2);\n}\n\nTEST_F(TestSendReturnPlugins, TestZeroDelayProcessing)\n{\n    ChunkSampleBuffer buffer_1(2);\n    ChunkSampleBuffer buffer_2(2);\n    test_utils::fill_sample_buffer(buffer_1, 1.0f);\n\n    // Set the send plugin to use the same thread as the return plugin\n    _send_instance.set_active_rt_processing(true, 1);\n\n    // Test that processing without destination doesn't break and passes though\n    _send_instance.process_audio(buffer_1, buffer_2);\n    test_utils::assert_buffer_value(1.0f, buffer_2);\n\n    _send_accessor.set_destination(&_return_instance);\n    _send_instance.process_audio(buffer_1, buffer_2);\n    buffer_2.clear();\n\n    // Don't swap, the send plugin should have immediately copied to the output without delay\n    _return_instance.process_audio(buffer_1, buffer_2);\n    test_utils::assert_buffer_value(1.0f, buffer_2);\n}\n\nTEST_F(TestSendReturnPlugins, TestMultipleSends)\n{\n    ChunkSampleBuffer buffer_1(2);\n    ChunkSampleBuffer buffer_2(2);\n    test_utils::fill_sample_buffer(buffer_1, 1.0f);\n\n    _host_control_mockup._transport.set_time(Time(0), 0);\n\n    _send_accessor.set_destination(&_return_instance);\n    _send_instance.process_audio(buffer_1, buffer_2);\n\n    SendPlugin send_instance_2(_host_ctrl, &_factory);\n    sushi::internal::send_plugin::Accessor _send_accessor_2 {send_instance_2};\n\n    _send_accessor_2.set_destination(&_return_instance);\n    send_instance_2.process_audio(buffer_1, buffer_2);\n    buffer_2.clear();\n\n    // Call process on the return, the buffers should not be swapped so output should be 0\n    _return_instance.process_audio(buffer_1, buffer_2);\n    test_utils::assert_buffer_value(0.0f, buffer_2);\n\n    // Fast forward time and call process again, buffers should now be swapped and we should\n    // read both sends on the output\n    _host_control_mockup._transport.set_time(Time(10), AUDIO_CHUNK_SIZE);\n    _return_instance.process_audio(buffer_1, buffer_2);\n    test_utils::assert_buffer_value(2.0f, buffer_2);\n}\n\nTEST_F(TestSendReturnPlugins, TestSelectiveChannelSending)\n{\n    auto channel_count_param_id = _send_instance.parameter_from_name(\"channel_count\")->id();\n    auto start_channel_param_id = _send_instance.parameter_from_name(\"start_channel\")->id();\n    auto dest_channel_param_id = _send_instance.parameter_from_name(\"dest_channel\")->id();\n\n\n    ChunkSampleBuffer buffer_1(2);\n    ChunkSampleBuffer buffer_2(2);\n    test_utils::fill_sample_buffer(buffer_1, 1.0f);\n\n    _send_instance.set_channels(2, 2);\n    _send_accessor.set_destination(&_return_instance);\n\n    // Send only 1 channel\n    auto event = RtEvent::make_parameter_change_event(_send_instance.id(), 0, channel_count_param_id,\n                                                      1.0f / (MAX_TRACK_CHANNELS - 1));\n    _send_instance.process_event(event);\n    _send_instance.process_audio(buffer_1, buffer_1);\n\n    // Swap manually and verify that signal only the first channel was sent\n    _return_accessor.swap_buffers();\n    _return_instance.process_audio(buffer_1, buffer_2);\n    EXPECT_FLOAT_EQ(1.0f, buffer_2.channel(0)[0]);\n    EXPECT_FLOAT_EQ(0.0f, buffer_2.channel(1)[0]);\n\n    // Set the destination channel to channel 1\n    event = RtEvent::make_parameter_change_event(_send_instance.id(), 0, dest_channel_param_id,\n                                                 1.0f / (MAX_TRACK_CHANNELS - 1));\n    _send_instance.process_event(event);\n    _send_instance.process_audio(buffer_1, buffer_1);\n\n    // Swap manually and verify that signal only the first channel was sent to channel 2\n    _return_accessor.swap_buffers();\n    _return_instance.process_audio(buffer_1, buffer_2);\n    EXPECT_FLOAT_EQ(0.0f, buffer_2.channel(0)[0]);\n    EXPECT_FLOAT_EQ(1.0f, buffer_2.channel(1)[0]);\n\n    // Set a destination channel outside the range of the return plugin's channel range\n    event = RtEvent::make_parameter_change_event(_send_instance.id(), 0, dest_channel_param_id, 1.0);\n    _send_instance.process_event(event);\n    _send_instance.process_audio(buffer_1, buffer_1);\n\n    // Both return channels should be 0\n    _return_accessor.swap_buffers();\n    _return_instance.process_audio(buffer_1, buffer_2);\n    EXPECT_FLOAT_EQ(0.0f, buffer_2.channel(0)[0]);\n    EXPECT_FLOAT_EQ(0.0f, buffer_2.channel(1)[0]);\n\n    // Send both channels the send plugin to channels 3 & 4 of the return plugin\n    _return_instance.set_channels(4, 4);\n\n    buffer_1.channel(0)[0] = 2.0;\n    buffer_1.channel(1)[0] = 3.0;\n    event = RtEvent::make_parameter_change_event(_send_instance.id(), 0, start_channel_param_id, 0);\n    _send_instance.process_event(event);\n    event = RtEvent::make_parameter_change_event(_send_instance.id(), 0, dest_channel_param_id,\n                                                 2.0f / (MAX_TRACK_CHANNELS - 1));\n    _send_instance.process_event(event);\n    event = RtEvent::make_parameter_change_event(_send_instance.id(), 0, channel_count_param_id,\n                                                 2.0f / (MAX_TRACK_CHANNELS - 1));\n    _send_instance.process_event(event);\n\n    _send_instance.process_audio(buffer_1, buffer_1);\n\n    buffer_1 = ChunkSampleBuffer(4);\n    buffer_2 = ChunkSampleBuffer(4);\n\n    _return_accessor.swap_buffers();\n    _return_instance.process_audio(buffer_1, buffer_2);\n    EXPECT_FLOAT_EQ(0.0f, buffer_2.channel(0)[0]);\n    EXPECT_FLOAT_EQ(0.0f, buffer_2.channel(1)[0]);\n    EXPECT_FLOAT_EQ(2.0f, buffer_2.channel(2)[0]);\n    EXPECT_FLOAT_EQ(3.0f, buffer_2.channel(3)[0]);\n}\n\nTEST_F(TestSendReturnPlugins, TestRampedProcessing)\n{\n    ChunkSampleBuffer buffer_1(2);\n    ChunkSampleBuffer buffer_2(2);\n    test_utils::fill_sample_buffer(buffer_1, 1.0f);\n\n    // Test only ramping\n    _return_instance.send_audio_with_ramp(buffer_1, 0, 2.0f, 0.0f, THREAD_ID_UNKNOWN);\n    _return_accessor.swap_buffers();\n    _return_instance.process_audio(buffer_1, buffer_2);\n    EXPECT_NEAR(2.0f, buffer_2.channel(0)[0], 0.01);\n    EXPECT_NEAR(1.0f, buffer_2.channel(0)[AUDIO_CHUNK_SIZE / 2], 0.1);\n    EXPECT_NEAR(0.0f, buffer_2.channel(0)[AUDIO_CHUNK_SIZE - 1], 0.01);\n    _return_accessor.swap_buffers();\n\n    // Test parameter smoothing\n    _send_accessor.set_destination(&_return_instance);\n    auto event = RtEvent::make_parameter_change_event(0, 0, 0, 0.0f);\n    _send_instance.process_event(event);\n    _send_instance.process_audio(buffer_1, buffer_2);\n    _return_accessor.swap_buffers();\n    _return_instance.process_audio(buffer_1, buffer_2);\n\n    // Audio should now begin to ramp down\n    EXPECT_FLOAT_EQ(1.0f, buffer_2.channel(0)[0]);\n    EXPECT_LT(buffer_2.channel(0)[AUDIO_CHUNK_SIZE -1], 1.0f);\n    EXPECT_GT(buffer_2.channel(0)[AUDIO_CHUNK_SIZE / 2], buffer_2.channel(0)[AUDIO_CHUNK_SIZE - 1]);\n }"
  },
  {
    "path": "test/unittests/plugins/step_sequencer_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"library/rt_event_fifo.h\"\n#include \"plugins/step_sequencer_plugin.cpp\"\n#include \"test_utils/host_control_mockup.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal::step_sequencer_plugin;\n\nconstexpr float TEST_SAMPLERATE = 48000;\n\nclass TestStepSequencerPlugin : public ::testing::Test\n{\nprotected:\n    TestStepSequencerPlugin() = default;\n\n    void SetUp() override\n    {\n        ProcessorReturnCode status = _module_under_test.init(TEST_SAMPLERATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n        _module_under_test.set_event_output(&_fifo);\n    }\n\n    RtEventFifo<10> _fifo;\n    HostControlMockup _host_control;\n    StepSequencerPlugin _module_under_test{_host_control.make_host_control_mockup(TEST_SAMPLERATE)};\n};\n\nTEST_F(TestStepSequencerPlugin, TestOutput)\n{\n    ChunkSampleBuffer buffer;\n    _host_control._transport.set_playing_mode(PlayingMode::PLAYING, false);\n    _host_control._transport.set_tempo(120.0f, false);\n    _host_control._transport.set_time(std::chrono::microseconds(0), 0);\n\n\n    ASSERT_TRUE(_fifo.empty());\n    /* 1/8 notes at 120 bpm equals 4 notes/sec, @48000 results having an\n     * 8th note at 12000, so fast-forward the time so directly before this time */\n    _host_control._transport.set_time(std::chrono::microseconds(249'500), 11'990);\n    _module_under_test.process_audio(buffer, buffer);\n    RtEvent e;\n    bool got_event = _fifo.pop(e);\n    ASSERT_TRUE(got_event);\n    ASSERT_EQ(_module_under_test.id(), e.processor_id());\n    ASSERT_EQ(RtEventType::NOTE_OFF, e.type());\n    _fifo.pop(e);\n    ASSERT_EQ(_module_under_test.id(), e.processor_id());\n    ASSERT_EQ(RtEventType::FLOAT_PARAMETER_CHANGE, e.type());\n    _fifo.pop(e);\n    ASSERT_EQ(_module_under_test.id(), e.processor_id());\n    ASSERT_EQ(RtEventType::FLOAT_PARAMETER_CHANGE, e.type());\n    _fifo.pop(e);\n    ASSERT_EQ(_module_under_test.id(), e.processor_id());\n    ASSERT_EQ(RtEventType::NOTE_ON, e.type());\n    ASSERT_EQ(48, e.keyboard_event()->note());\n    ASSERT_TRUE(_fifo.empty());\n}\n"
  },
  {
    "path": "test/unittests/plugins/wav_streamer_plugin_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include \"test_utils/test_utils.h\"\n#include \"test_utils/host_control_mockup.h\"\n#include \"library/rt_event_fifo.h\"\n\n#include \"elk-warning-suppressor/warning_suppressor.hpp\"\n\n#include \"plugins/wav_streamer_plugin.cpp\"\n\nusing namespace sushi;\nusing namespace sushi::internal::wav_streamer_plugin;\n\nconstexpr float TEST_SAMPLERATE = 48000;\nconstexpr int   TEST_CHANNEL_COUNT = 2;\n\nstatic const std::string SAMPLE_FILE = \"Kawai-K11-GrPiano-C4_mono.wav\";\nconstexpr float SAMPLE_FILE_LENGTH = 1.779546485f;\n\nTEST(TestWaveStreamerPluginInternal, TestGainScaleFunction)\n{\n    EXPECT_NEAR(0.125f, exp_approx(0.5f, 1.0f), 0.001f);\n    EXPECT_NEAR(0.015625, exp_approx(0.25f, 1.0f), 0.001f);\n    EXPECT_NEAR(0.25f, exp_approx(1.0f, 2.0f), 0.001f);\n    EXPECT_NEAR(0.0625f, exp_approx(0.25f, 0.5f), 0.001f);\n}\n\nclass TestWaveStreamerPlugin : public ::testing::Test\n{\nprotected:\n    void SetUp() override\n    {\n        _module_under_test = std::make_unique<WavStreamerPlugin>(_host_control.make_host_control_mockup(TEST_SAMPLERATE));\n        ProcessorReturnCode status = _module_under_test->init(TEST_SAMPLERATE);\n        ASSERT_EQ(ProcessorReturnCode::OK, status);\n        _module_under_test->set_channels(0, TEST_CHANNEL_COUNT);\n        _module_under_test->set_enabled(true);\n        _module_under_test->set_event_output(&_fifo);\n    }\n\n    void LoadFile(const std::string& file)\n    {\n        auto path = std::string(test_utils::get_data_dir_path().append(file));\n\n        auto res = _module_under_test->set_property_value(FILE_PROPERTY_ID, path);\n        ASSERT_EQ(ProcessorReturnCode::OK, res);\n    }\n\n    HostControlMockup _host_control;\n    std::unique_ptr<WavStreamerPlugin> _module_under_test;\n    RtSafeRtEventFifo _fifo;\n};\n\nTEST_F(TestWaveStreamerPlugin, TestInstantiation)\n{\n    ASSERT_TRUE(_module_under_test.get());\n    ASSERT_EQ(\"Wav Streamer\", _module_under_test->label());\n    ASSERT_EQ(\"sushi.testing.wav_streamer\", _module_under_test->name());\n\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(TEST_CHANNEL_COUNT);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(TEST_CHANNEL_COUNT);\n    _module_under_test->process_audio(in_buffer, out_buffer);\n    test_utils::assert_buffer_value(0.0, out_buffer);\n}\n\nTEST_F(TestWaveStreamerPlugin, TestWaveLoadingAndPlaying)\n{\n    auto playing_param_id = _module_under_test->parameter_from_name(\"playing\")->id();\n    auto len_param_id = _module_under_test->parameter_from_name(\"length\")->id();\n    auto pos_param_id = _module_under_test->parameter_from_name(\"position\")->id();\n\n    // Load non-existing file\n    LoadFile(\"NO FILE\");\n    EXPECT_EQ(\"Error:\", _module_under_test->property_value(FILE_PROPERTY_ID).second.substr(0, 6));\n    EXPECT_EQ(0.0f, _module_under_test->parameter_value(len_param_id).second);\n    EXPECT_EQ(0.0f, _module_under_test->parameter_value(pos_param_id).second);\n\n    // Load actual file and verify we got something on the output\n    LoadFile(SAMPLE_FILE);\n    _module_under_test->process_event(RtEvent::make_parameter_change_event(0, 0, playing_param_id, 1.0f));\n\n    SampleBuffer<AUDIO_CHUNK_SIZE> in_buffer(TEST_CHANNEL_COUNT);\n    SampleBuffer<AUDIO_CHUNK_SIZE> out_buffer(TEST_CHANNEL_COUNT);\n    for (int i = 0; i <= SEEK_UPDATE_INTERVAL; ++i)\n    {\n        _module_under_test->process_audio(in_buffer, out_buffer);\n        test_utils::assert_buffer_non_null(out_buffer);\n    }\n\n    // Verify that output parameters are updated\n    EXPECT_FLOAT_EQ(SAMPLE_FILE_LENGTH / MAX_FILE_LENGTH, _module_under_test->parameter_value(len_param_id).second);\n    EXPECT_EQ(\"1.78\", _module_under_test->parameter_value_formatted(len_param_id).second);\n    EXPECT_NE(0.0f, _module_under_test->parameter_value(pos_param_id).second);\n    EXPECT_EQ(1.0f, _module_under_test->parameter_value(playing_param_id).second);\n\n    // Load non-existing file again and verify that playback stops and parameters are updated.\n    LoadFile(\"NO FILE\");\n    for (int i = 0; i <= + SEEK_UPDATE_INTERVAL * 2 ; ++i)\n    {\n        _module_under_test->process_audio(in_buffer, out_buffer);\n    }\n    EXPECT_EQ(0.0f, _module_under_test->parameter_value(len_param_id).second);\n    EXPECT_EQ(0.0f, _module_under_test->parameter_value(pos_param_id).second);\n    EXPECT_EQ(0.0f, _module_under_test->parameter_value(playing_param_id).second);\n}\n\n\n"
  },
  {
    "path": "test/unittests/sample_test.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n// A simple test case\nTEST (SampleTest, SimpleTestCase) {\n    ASSERT_TRUE (1);\n}\n\n// A more complex test case where tests can be grouped\n// And setup and teardown functions added.\nclass SampleTestCase : public ::testing::Test\n{\nprotected:\n    SampleTestCase() = default;\n\n    void SetUp() override {}\n\n    void TearDown() override {}\n};\n\nTEST_F(SampleTestCase, SampleTest)\n{\n    EXPECT_FALSE(0);\n}\n"
  },
  {
    "path": "test/unittests/test_utils/apple_coreaudio_mockup.cpp",
    "content": "#include <CoreAudio/AudioHardware.h>\n\n#pragma clang diagnostic push\n#pragma clang diagnostic ignored \"-Wnullability-completeness\"// Ignore Apple nonsense\n#include \"apple_coreaudio_mockup.h\"\n#pragma clang diagnostic pop\n\nAppleAudioHardwareMockup* AppleAudioHardwareMockup::instance = nullptr;\n\nOSStatus AudioDeviceCreateIOProcID(AudioObjectID audio_object_id,\n                                   AudioDeviceIOProc io_proc,\n                                   void* client_data,\n                                   AudioDeviceIOProcID* proc_id)\n{\n    return AppleAudioHardwareMockup::instance->AudioDeviceCreateIOProcID(\n            audio_object_id,\n            io_proc,\n            client_data,\n            proc_id);\n}\n\nOSStatus AudioDeviceDestroyIOProcID(AudioObjectID audio_object_id, AudioDeviceIOProcID proc_id)\n{\n    return AppleAudioHardwareMockup::instance->AudioDeviceDestroyIOProcID(audio_object_id, proc_id);\n}\n\nOSStatus AudioDeviceStart(AudioObjectID audio_object_id, AudioDeviceIOProcID proc_id)\n{\n    return AppleAudioHardwareMockup::instance->AudioDeviceStart(audio_object_id, proc_id);\n}\n\nOSStatus AudioDeviceStop(AudioObjectID audio_object_id, AudioDeviceIOProcID proc_id)\n{\n    return AppleAudioHardwareMockup::instance->AudioDeviceStop(audio_object_id, proc_id);\n}\n\nOSStatus AudioObjectGetPropertyData(AudioObjectID audio_object_id,\n                                    const AudioObjectPropertyAddress* address,\n                                    UInt32 qualifier_data_size,\n                                    const void* __nullable qualifier_data,\n                                    UInt32* data_size,\n                                    void* out_data)\n{\n    return AppleAudioHardwareMockup::instance->AudioObjectGetPropertyData(\n            audio_object_id,\n            address,\n            qualifier_data_size,\n            qualifier_data,\n            data_size,\n            out_data);\n}\n\nOSStatus AudioObjectSetPropertyData(AudioObjectID audio_object_id,\n                                    const AudioObjectPropertyAddress* address,\n                                    UInt32 qualifier_data_size,\n                                    const void* __nullable qualifier_data,\n                                    UInt32 data_size,\n                                    const void* data)\n{\n    return AppleAudioHardwareMockup::instance->AudioObjectSetPropertyData(\n            audio_object_id,\n            address,\n            qualifier_data_size,\n            qualifier_data,\n            data_size,\n            data);\n}\n\nOSStatus AudioObjectGetPropertyDataSize(AudioObjectID audio_object_id,\n                                        const AudioObjectPropertyAddress* address,\n                                        UInt32 qualifier_data_size,\n                                        const void* __nullable qualifier_data,\n                                        UInt32* out_data_size)\n{\n    return AppleAudioHardwareMockup::instance->AudioObjectGetPropertyDataSize(\n            audio_object_id,\n            address,\n            qualifier_data_size,\n            qualifier_data,\n            out_data_size);\n}\n\nBoolean AudioObjectHasProperty(AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address)\n{\n    return AppleAudioHardwareMockup::instance->AudioObjectHasProperty(audio_object_id, address);\n}\n\nOSStatus AudioObjectIsPropertySettable(AudioObjectID audio_object_id,\n                                       const AudioObjectPropertyAddress* address,\n                                       Boolean* out_is_settable)\n{\n    return AppleAudioHardwareMockup::instance->AudioObjectIsPropertySettable(audio_object_id, address, out_is_settable);\n}\n\nOSStatus AudioObjectAddPropertyListener(AudioObjectID audio_object_id,\n                                        const AudioObjectPropertyAddress* address,\n                                        AudioObjectPropertyListenerProc listener,\n                                        void* __nullable client_data)\n{\n    return AppleAudioHardwareMockup::instance->AudioObjectAddPropertyListener(audio_object_id, address, listener, client_data);\n}\n\nOSStatus AudioObjectRemovePropertyListener(AudioObjectID audio_object_id,\n                                           const AudioObjectPropertyAddress* address,\n                                           AudioObjectPropertyListenerProc listener,\n                                           void* __nullable client_data)\n{\n    return AppleAudioHardwareMockup::instance->AudioObjectRemovePropertyListener(audio_object_id, address, listener, client_data);\n}\n"
  },
  {
    "path": "test/unittests/test_utils/apple_coreaudio_mockup.h",
    "content": "#ifndef SUSHI_APPLE_COREAUDIO_MOCKUP_H\n#define SUSHI_APPLE_COREAUDIO_MOCKUP_H\n\n#include <CoreAudio/AudioHardware.h>\n\n#include <gmock/gmock.h>\n\nclass AppleAudioHardwareMockup\n{\npublic:\n    MOCK_METHOD(Boolean, AudioObjectHasProperty, (AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address));\n    MOCK_METHOD(OSStatus, AudioObjectGetPropertyData, (AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32 qualifier_data_size, const void* qualifier_data, UInt32* data_size, void* data));\n    MOCK_METHOD(OSStatus, AudioObjectSetPropertyData, (AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32 qualifier_data_size, const void* qualifier_data, UInt32 data_size, const void* data));\n    MOCK_METHOD(OSStatus, AudioObjectGetPropertyDataSize, (AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, UInt32 qualifier_data_size, const void* qualifier_data, UInt32* out_data_size));\n    MOCK_METHOD(OSStatus, AudioObjectIsPropertySettable, (AudioObjectID audio_object_id, const AudioObjectPropertyAddress* address, Boolean* out_is_settable));\n\n    MOCK_METHOD(OSStatus, AudioDeviceCreateIOProcID, (AudioObjectID audio_object_id, AudioDeviceIOProc io_proc, void* client_data, AudioDeviceIOProcID* io_proc_id));\n    MOCK_METHOD(OSStatus, AudioDeviceDestroyIOProcID, (AudioObjectID audio_object_id, AudioDeviceIOProcID proc_id));\n    MOCK_METHOD(OSStatus, AudioDeviceStart, (AudioObjectID audio_object_id, AudioDeviceIOProcID proc_id));\n    MOCK_METHOD(OSStatus, AudioDeviceStop, (AudioObjectID audio_object_id, AudioDeviceIOProcID proc_id));\n\n    MOCK_METHOD(OSStatus, AudioObjectAddPropertyListener, (AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, AudioObjectPropertyListenerProc inListener, void* __nullable inClientData));\n    MOCK_METHOD(OSStatus, AudioObjectRemovePropertyListener, (AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, AudioObjectPropertyListenerProc inListener, void* __nullable inClientData));\n\n    static AppleAudioHardwareMockup* instance;\n};\n\n#endif// SUSHI_APPLE_COREAUDIO_MOCKUP_H\n"
  },
  {
    "path": "test/unittests/test_utils/audio_frontend_mockup.h",
    "content": "#ifndef SUSHI_AUDIO_FRONTEND_MOCKUP_H\n#define SUSHI_AUDIO_FRONTEND_MOCKUP_H\n\n#include <gmock/gmock.h>\n\n#include \"audio_frontends/base_audio_frontend.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::audio_frontend;\n\nclass AudioFrontendMockup : public BaseAudioFrontend\n{\npublic:\n    AudioFrontendMockup() : BaseAudioFrontend(nullptr) {}\n\n    MOCK_METHOD(void,\n                cleanup,\n                (),\n                (override));\n\n    MOCK_METHOD(void,\n                run,\n                (),\n                (override));\n\n    MOCK_METHOD(void,\n                pause,\n                (bool paused),\n                (override));\n};\n\n#endif //SUSHI_AUDIO_FRONTEND_MOCKUP_H\n"
  },
  {
    "path": "test/unittests/test_utils/audio_graph_accessor.h",
    "content": "#ifndef SUSHI_LIBRARY_AUDIO_GRAPH_ACCESSOR_H\n#define SUSHI_LIBRARY_AUDIO_GRAPH_ACCESSOR_H\n\n#include \"engine/audio_graph.h\"\n\nnamespace sushi::internal::engine\n{\n\nclass AudioGraphAccessor\n{\npublic:\n    explicit AudioGraphAccessor(AudioGraph& f) : _friend(f) {}\n\n    [[nodiscard]] const std::vector<AudioGraph::GraphNode>& audio_graph()\n    {\n        return _friend._audio_graph;\n    }\n\nprivate:\n    AudioGraph& _friend;\n};\n\n} // end namespace sushi::internal::engine\n\n#endif //SUSHI_LIBRARY_AUDIO_GRAPH_ACCESSOR_H\n"
  },
  {
    "path": "test/unittests/test_utils/control_mockup.h",
    "content": "#ifndef SUSHI_CONTROL_MOCKUP_H\n#define SUSHI_CONTROL_MOCKUP_H\n\n#if !defined __clang__ && !defined _MSC_VER\n#include <bits/stdc++.h>\n#endif\n\n#include <unordered_map>\n\n#include \"sushi/control_interface.h\"\n\nnamespace sushi::control {\n\nconst ParameterInfo parameter_1{0, ParameterType::INT, \"param 1\", \"param 1\", \"unit\", false, 0, 0};\nconst ParameterInfo parameter_2{1, ParameterType::FLOAT, \"param 2\", \"param 2\", \"unit\", true, 1, 1};\nconst ParameterInfo parameter_3{2, ParameterType::BOOL, \"param 3\", \"param 3\", \"unit\", false, -1, -1};\nconst PropertyInfo property_1{1, \"property_1\", \"Property 1\"};\nconst std::vector<ParameterInfo> parameters{parameter_1, parameter_2, parameter_3};\n\nconst ProcessorInfo processor_1{0, \"proc 1\", \"proc 1\", 0 , 0};\nconst ProcessorInfo processor_2{1, \"proc 2\", \"proc 2\", 1 , 1};\nconst std::vector<ProcessorInfo> processors{processor_1, processor_2};\n\nconst TrackInfo track1{0, \"track 1\", \"track 1\", 0, 0, 0, TrackType::REGULAR, {}};\nconst TrackInfo track2{1, \"track 2\", \"track 2\", 1, 1, 1, TrackType::REGULAR, {}};\nconst std::vector<TrackInfo> tracks{track1, track2};\n\nconstexpr float                 DEFAULT_SAMPLERATE = 48000.0f;\nconstexpr float                 DEFAULT_TEMPO = 120.0f;\nconstexpr float                 DEFAULT_PARAMETER_VALUE = 0.745f;\nconstexpr auto                  DEFAULT_STRING_PROPERTY = \"string property\";\nconstexpr bool                  DEFAULT_TIMING_STATISTICS_ENABLED = false;\nconstexpr bool                  DEFAULT_BYPASS_STATE = false;\nconstexpr PlayingMode           DEFAULT_PLAYING_MODE = PlayingMode::PLAYING;\nconstexpr SyncMode              DEFAULT_SYNC_MODE = SyncMode::INTERNAL;\nconstexpr TimeSignature         DEFAULT_TIME_SIGNATURE = TimeSignature{4, 4};\nconstexpr ControlStatus         DEFAULT_CONTROL_STATUS = ControlStatus::OK;\nconstexpr Timings               DEFAULT_TIMINGS = Timings{1.0f, 0.5f, 1.5f};\nconstexpr int                   DEFAULT_PROGRAM_ID = 1;\nconstexpr auto                  DEFAULT_PROGRAM_NAME = \"program 1\";\nconst std::vector<std::string>  DEFAULT_PROGRAMS = {DEFAULT_PROGRAM_NAME, \"program 2\"};\n\n\nclass TestableController\n{\npublic:\n    std::unordered_map<std::string, std::string> get_args_from_last_call()\n    {\n        return _args_from_last_call;\n    }\n\n    bool was_recently_called()\n    {\n        return _recently_called;\n    }\n\n    void clear_recent_call()\n    {\n        _recently_called = false;\n    }\n\n    void force_return_status(ControlStatus status)\n    {\n        _return_status = status;\n    }\n\nprotected:\n    TestableController() = default;\n\n    std::unordered_map<std::string,std::string> _args_from_last_call;\n    ControlStatus _return_status{DEFAULT_CONTROL_STATUS};\n    int  _return_id;\n    bool _recently_called{false};\n};\n\nclass SystemControllerMockup : public SystemController, public TestableController\n{\npublic:\n    std::string get_sushi_version() const override {return \"\";}\n\n    std::string get_sushi_api_version() const override {return \"\";}\n\n    SushiBuildInfo get_sushi_build_info() const override {return SushiBuildInfo();}\n\n    int get_input_audio_channel_count() const override {return 0;}\n\n    int get_output_audio_channel_count() const override {return 0;}\n};\n\nclass TransportControllerMockup : public TransportController, public TestableController\n{\npublic:\n    float get_samplerate() const override {return DEFAULT_SAMPLERATE;}\n\n    PlayingMode get_playing_mode() const override {return DEFAULT_PLAYING_MODE;}\n\n    SyncMode get_sync_mode() const override {return DEFAULT_SYNC_MODE;}\n\n    TimeSignature get_time_signature() const override {return DEFAULT_TIME_SIGNATURE;}\n\n    float get_tempo() const override {return DEFAULT_TEMPO;}\n\n    ControlStatus set_sync_mode(SyncMode sync_mode) override\n    {\n        _args_from_last_call.clear();\n        switch (sync_mode)\n        {\n            case control::SyncMode::GATE:       _args_from_last_call[\"sync mode\"] = \"GATE\"; break;\n            case control::SyncMode::INTERNAL:   _args_from_last_call[\"sync mode\"] = \"INTERNAL\"; break;\n            case control::SyncMode::LINK:       _args_from_last_call[\"sync mode\"] = \"LINK\"; break;\n            case control::SyncMode::MIDI:       _args_from_last_call[\"sync mode\"] = \"MIDI\"; break;\n            default:                            _args_from_last_call[\"sync mode\"] = \"UNKNOWN MODE\";\n        }\n\n        _recently_called = true;\n        return _return_status;\n    };\n\n    void set_playing_mode(PlayingMode playing_mode) override\n    {\n        _args_from_last_call.clear();\n        switch (playing_mode)\n        {\n            case control::PlayingMode::STOPPED:     _args_from_last_call[\"playing mode\"] = \"STOPPED\"; break;\n            case control::PlayingMode::RECORDING:   _args_from_last_call[\"playing mode\"] = \"RECORDING\"; break;\n            case control::PlayingMode::PLAYING:     _args_from_last_call[\"playing mode\"] = \"PLAYING\"; break;\n            default:                                _args_from_last_call[\"playing mode\"] = \"UNKWON MODE\";\n        }\n        _recently_called = true;\n    };\n\n    ControlStatus set_tempo(float tempo) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"tempo\"] = std::to_string(tempo);\n        _recently_called = true;\n        return DEFAULT_CONTROL_STATUS;\n    };\n\n    ControlStatus set_time_signature(TimeSignature signature) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"numerator\"] = std::to_string(signature.numerator);\n        _args_from_last_call[\"denominator\"] = std::to_string(signature.denominator);\n        _recently_called = true;\n        return DEFAULT_CONTROL_STATUS;\n    };\n};\n\nclass TimingControllerMockup : public TimingController, public TestableController\n{\npublic:\n    bool get_timing_statistics_enabled() const override\n    {\n        return DEFAULT_TIMING_STATISTICS_ENABLED;\n    }\n\n    void set_timing_statistics_enabled(bool enabled) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"enabled\"] = std::to_string(enabled);\n        _recently_called = true;\n    };\n\n    std::pair<ControlStatus, CpuTimings> get_engine_timings() const override\n    {\n        return {_return_status, {DEFAULT_TIMINGS, {}}};\n    }\n\n    std::pair<ControlStatus, Timings> get_track_timings(int /*track_id*/) const override\n    {\n        return {_return_status, DEFAULT_TIMINGS};\n    }\n\n    std::pair<ControlStatus, Timings> get_processor_timings(int /*processor_id*/) const override\n    {\n        return {_return_status, DEFAULT_TIMINGS};\n    }\n\n    ControlStatus reset_all_timings() override\n    {\n        _recently_called = true;\n        return _return_status;\n    }\n\n    ControlStatus reset_track_timings(int track_id) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"track_id\"] = std::to_string(track_id);\n        _recently_called = true;\n        return _return_status;\n    }\n\n    ControlStatus reset_processor_timings(int processor_id) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"processor_id\"] = std::to_string(processor_id);\n        _recently_called = true;\n        return _return_status;\n    }\n\n};\n\nclass KeyboardControllerMockup : public KeyboardController, public TestableController\n{\npublic:\n    ControlStatus send_note_on(int track_id, int channel, int note, float velocity) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"track id\"] = std::to_string(track_id);\n        _args_from_last_call[\"channel\"] = std::to_string(channel);\n        _args_from_last_call[\"note\"] = std::to_string(note);\n        _args_from_last_call[\"velocity\"] = std::to_string(velocity);\n        _recently_called = true;\n        return _return_status;\n    }\n\n    ControlStatus send_note_off(int track_id, int channel, int note, float velocity) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"track id\"] = std::to_string(track_id);\n        _args_from_last_call[\"channel\"] = std::to_string(channel);\n        _args_from_last_call[\"note\"] = std::to_string(note);\n        _args_from_last_call[\"velocity\"] = std::to_string(velocity);\n        _recently_called = true;\n        return _return_status;\n    }\n\n    ControlStatus send_note_aftertouch(int track_id, int channel, int note, float value) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"track id\"] = std::to_string(track_id);\n        _args_from_last_call[\"channel\"] = std::to_string(channel);\n        _args_from_last_call[\"note\"] = std::to_string(note);\n        _args_from_last_call[\"value\"] = std::to_string(value);\n        _recently_called = true;\n        return _return_status;\n    }\n\n    ControlStatus send_aftertouch(int track_id, int channel, float value) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"track id\"] = std::to_string(track_id);\n        _args_from_last_call[\"channel\"] = std::to_string(channel);\n        _args_from_last_call[\"value\"] = std::to_string(value);\n        _recently_called = true;\n        return _return_status;\n    }\n\n    ControlStatus send_pitch_bend(int track_id, int channel, float value) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"track id\"] = std::to_string(track_id);\n        _args_from_last_call[\"channel\"] = std::to_string(channel);\n        _args_from_last_call[\"value\"] = std::to_string(value);\n        _recently_called = true;\n        return _return_status;\n    }\n\n    ControlStatus send_modulation(int track_id, int channel, float value) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"track id\"] = std::to_string(track_id);\n        _args_from_last_call[\"channel\"] = std::to_string(channel);\n        _args_from_last_call[\"value\"] = std::to_string(value);\n        _recently_called = true;\n        return _return_status;\n    }\n};\n\nclass AudioGraphControllerMockup : public AudioGraphController, public TestableController\n{\npublic:\n    std::vector<ProcessorInfo> get_all_processors() const override {return processors;}\n\n    std::vector<TrackInfo> get_all_tracks() const override {return tracks;}\n\n    std::pair<ControlStatus, int> get_track_id(const std::string& /*track_name*/) const override\n    {\n        return {_return_status, track1.id};\n    }\n\n    std::pair<ControlStatus, TrackInfo> get_track_info(int /*track_id*/) const override\n    {\n        return {_return_status, track1};\n    }\n\n    std::pair<ControlStatus, std::vector<ProcessorInfo>> get_track_processors(int /*track_id*/) const override\n    {\n        return {_return_status, processors};\n    }\n\n    std::pair<ControlStatus, int> get_processor_id(const std::string& /*processor_name*/) const override\n    {\n        return {_return_status, processor_1.id};\n    }\n\n    std::pair<ControlStatus, ProcessorInfo> get_processor_info(int /*processor_id*/) const override\n    {\n        return {_return_status, processor_1};\n    }\n\n    std::pair<ControlStatus, bool> get_processor_bypass_state(int /*processor_id*/) const override\n    {\n        return {_return_status, DEFAULT_BYPASS_STATE};\n    }\n\n    std::pair<ControlStatus, control::ProcessorState> get_processor_state(int /*processor_id*/) const override\n    {\n        return {_return_status, control::ProcessorState()};\n    }\n\n    ControlResponse set_processor_bypass_state(int processor_id, bool bypass_enabled) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"processor id\"] = std::to_string(processor_id);\n        _args_from_last_call[\"bypass enabled\"] = std::to_string(bypass_enabled);\n        _recently_called = true;\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse set_processor_state(int processor_id, const control::ProcessorState& /*state*/) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"processor id\"] = std::to_string(processor_id);\n        _recently_called = true;\n        return {_return_status, _return_id};    }\n\n    ControlResponse create_track(const std::string& name, int channels, std::optional<int> thread) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"name\"] = name;\n        _args_from_last_call[\"channels\"] = std::to_string(channels);\n        _recently_called = true;\n        return {_return_status, _return_id};    }\n\n    ControlResponse create_multibus_track(const std::string& name, int buses, std::optional<int> thread) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"name\"] = name;\n        _args_from_last_call[\"buses\"] = std::to_string(buses);\n        _recently_called = true;\n        return {_return_status, _return_id};    }\n\n    ControlResponse create_pre_track(const std::string& name) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"name\"] = name;\n        _recently_called = true;\n        return {_return_status, _return_id};    }\n\n    ControlResponse create_post_track(const std::string& name) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"name\"] = name;\n        _recently_called = true;\n        return {_return_status, _return_id};    }\n\n\n    ControlResponse move_processor_on_track(int processor_id,\n                                            int source_track_id,\n                                            int dest_track_id,\n                                            std::optional<int> before_processor_id) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"processor_id\"] = std::to_string(processor_id);\n        _args_from_last_call[\"source_track_id\"] = std::to_string(source_track_id);\n        _args_from_last_call[\"dest_track_id\"] = std::to_string(dest_track_id);\n        _args_from_last_call[\"before_processor_id\"] = std::to_string(before_processor_id.value_or(-1));\n        _recently_called = true;\n        return {_return_status, _return_id};    }\n\n    ControlResponse create_processor_on_track(const std::string& name,\n                                              const std::string& uid,\n                                              const std::string& file,\n                                              PluginType type,\n                                              int track_id,\n                                              std::optional<int> before_processor_id) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"name\"] = name;\n        _args_from_last_call[\"uid\"] = uid;\n        _args_from_last_call[\"file\"] = file;\n        _args_from_last_call[\"type\"] = std::to_string(static_cast<int>(type));\n        _args_from_last_call[\"track_id\"] = std::to_string(track_id);\n        _args_from_last_call[\"before_processor_id\"] = std::to_string(before_processor_id.value_or(-1));\n        _recently_called = true;\n        return {_return_status, _return_id};    }\n\n    ControlResponse delete_processor_from_track(int processor_id, int track_id) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"processor_id\"] = std::to_string(processor_id);\n        _args_from_last_call[\"track_id\"] = std::to_string(track_id);\n        _recently_called = true;\n        return {_return_status, _return_id};    }\n\n    ControlResponse delete_track(int track_id) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"track_id\"] = std::to_string(track_id);\n        _recently_called = true;\n        return {_return_status, _return_id};    }\n};\n\nclass ProgramControllerMockup : public ProgramController, public TestableController\n{\npublic:\n    std::pair<ControlStatus, int> get_processor_current_program(int /*processor_id*/) const override\n    {\n        return {_return_status, DEFAULT_PROGRAM_ID};\n    }\n\n    std::pair<ControlStatus, std::string> get_processor_current_program_name(int /*processor_id*/) const override\n    {\n        return {_return_status, DEFAULT_PROGRAM_NAME};\n    }\n\n    std::pair<ControlStatus, std::string> get_processor_program_name(int /*processor_id*/, int /*program_id*/) const override\n    {\n        return {_return_status, DEFAULT_PROGRAM_NAME};\n    }\n\n    std::pair<ControlStatus, std::vector<std::string>> get_processor_programs(int /*processor_id*/) const override\n    {\n        return {_return_status, DEFAULT_PROGRAMS};\n    }\n\n    ControlResponse set_processor_program(int processor_id, int program_id) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"processor id\"] = std::to_string(processor_id);\n        _args_from_last_call[\"program id\"] = std::to_string(program_id);\n        _recently_called = true;\n        return {_return_status, _return_id};    }\n};\n\nclass ParameterControllerMockup : public ParameterController, public TestableController\n{\n    std::pair<ControlStatus, std::vector<ParameterInfo>> get_processor_parameters(int /*processor_id*/) const override\n    {\n        return {_return_status, parameters};\n    }\n\n    std::pair<ControlStatus, std::vector<ParameterInfo>> get_track_parameters(int /*processor_id*/) const override\n    {\n        return {_return_status, parameters};\n    }\n\n    std::pair<ControlStatus, int> get_parameter_id(int /*processor_id*/, const std::string& /*parameter*/) const override\n    {\n        return {_return_status, parameter_1.id};\n    }\n\n    std::pair<ControlStatus, ParameterInfo> get_parameter_info(int /*processor_id*/, int /*parameter_id*/) const override\n    {\n        return {_return_status, parameter_1};\n    }\n\n    std::pair<ControlStatus, float> get_parameter_value(int /*processor_id*/, int /*parameter_id*/) const override\n    {\n        return {_return_status, DEFAULT_PARAMETER_VALUE};\n    }\n\n    std::pair<ControlStatus, float> get_parameter_value_in_domain(int /*processor_id*/, int /*parameter_id*/) const override\n    {\n        return {_return_status, DEFAULT_PARAMETER_VALUE};\n    }\n\n    std::pair<ControlStatus, std::string> get_parameter_value_as_string(int /*processor_id*/, int /*parameter_id*/) const override\n    {\n        return {_return_status, std::to_string(DEFAULT_PARAMETER_VALUE)};\n    }\n\n    ControlStatus set_parameter_value(int processor_id, int parameter_id, float value) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"processor id\"] = std::to_string(processor_id);\n        _args_from_last_call[\"parameter id\"] = std::to_string(parameter_id);\n        _args_from_last_call[\"value\"] = std::to_string(value);\n        _recently_called = true;\n        return _return_status;\n    }\n\n    std::pair<ControlStatus, std::vector<PropertyInfo>> get_processor_properties(int /*processor_id*/) const override\n    {\n        return {ControlStatus::OK, std::vector<PropertyInfo>({property_1})};\n    }\n\n    std::pair<ControlStatus, std::vector<PropertyInfo>> get_track_properties(int /*processor_id*/) const override\n    {\n        return {ControlStatus::OK, std::vector<PropertyInfo>({property_1})};\n    }\n\n    std::pair<ControlStatus, int> get_property_id(int /*processor_id*/, const std::string& /*property_id*/) const override\n    {\n        return {ControlStatus::OK, 0};\n    }\n    std::pair<ControlStatus, PropertyInfo> get_property_info(int /*processor_id*/, int /*property_id*/) const override\n    {\n        return {ControlStatus::OK, property_1};\n    }\n\n    std::pair<ControlStatus, std::string> get_property_value(int /*processor_id*/, int /*property_id*/) const override\n    {\n        return {_return_status, DEFAULT_STRING_PROPERTY};\n    }\n\n    ControlStatus set_property_value(int processor_id, int property_id, const std::string& value) override\n    {\n        _args_from_last_call.clear();\n        _args_from_last_call[\"processor id\"] = std::to_string(processor_id);\n        _args_from_last_call[\"property id\"] = std::to_string(property_id);\n        _args_from_last_call[\"value\"] = value;\n        _recently_called = true;\n        return _return_status;\n    }\n};\n\nclass MidiControllerMockup : public MidiController, public TestableController\n{\npublic:\n    int get_input_ports() const override {return 0;}\n\n    int get_output_ports() const override {return 0;}\n\n    std::vector<MidiKbdConnection> get_all_kbd_input_connections() const override\n    {\n        return std::vector<MidiKbdConnection>();\n    }\n\n    std::vector<MidiKbdConnection> get_all_kbd_output_connections() const override\n    {\n        return std::vector<MidiKbdConnection>();\n    }\n\n    std::vector<MidiCCConnection> get_all_cc_input_connections() const override\n    {\n        return std::vector<MidiCCConnection>();\n    }\n\n    std::vector<MidiPCConnection> get_all_pc_input_connections() const override\n    {\n        return std::vector<MidiPCConnection>();\n    }\n\n    std::pair<ControlStatus, std::vector<MidiCCConnection>>\n    get_cc_input_connections_for_processor(int /*processor_id*/) const override\n    {\n        return {_return_status, std::vector<MidiCCConnection>()};\n    }\n\n    std::pair<ControlStatus, std::vector<MidiPCConnection>>\n    get_pc_input_connections_for_processor(int /*processor_id*/) const override\n    {\n        return {_return_status, std::vector<MidiPCConnection>()};\n    }\n\n    bool get_midi_clock_output_enabled(int /*port*/) const override\n    {\n        return false;\n    }\n\n    ControlStatus set_midi_clock_output_enabled(bool /*enabled*/, int /*port*/) override\n    {\n        return _return_status;\n    }\n\n    ControlResponse connect_kbd_input_to_track(int /*track_id*/, MidiChannel /*channel*/, int /*port*/, bool /*raw_midi*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse connect_kbd_output_from_track(int /*track_id*/, MidiChannel /*channel*/, int /*port*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse connect_cc_to_parameter(int /*processor_id*/,\n                                           int /*parameter_id*/,\n                                           MidiChannel /*channel*/,\n                                           int /*port*/,\n                                           int /*cc_number*/,\n                                           float /*min_range*/,\n                                           float /*max_range*/,\n                                           bool /*relative_mode*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse connect_pc_to_processor(int /*processor_id*/, MidiChannel /*channel*/, int /*port*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_kbd_input(int /*track_id*/, MidiChannel /*channel*/, int /*port*/, bool /*raw_midi*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_kbd_output(int /*track_id*/, MidiChannel /*channel*/, int /*port*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_cc(int /*processor_id*/, MidiChannel /*channel*/, int /*port*/, int /*cc_number*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_pc(int /*processor_id*/, MidiChannel /*channel*/, int /*port*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_all_cc_from_processor(int /*processor_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_all_pc_from_processor(int /*processor_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n};\n\nclass AudioRoutingControllerMockup : public AudioRoutingController, public TestableController\n{\npublic:\n    std::vector<AudioConnection> get_all_input_connections() const override\n    {\n        return std::vector<AudioConnection>();\n    }\n\n    std::vector<AudioConnection> get_all_output_connections() const override\n    {\n        return std::vector<AudioConnection>();\n    }\n\n    std::pair<ControlStatus, std::vector<AudioConnection>> get_input_connections_for_track(int /*track_id*/) const override\n    {\n        return {_return_status, std::vector<AudioConnection>()};\n    }\n\n    std::pair<ControlStatus, std::vector<AudioConnection>> get_output_connections_for_track(int /*track_id*/) const override\n    {\n        return {_return_status, std::vector<AudioConnection>()};\n    }\n\n    ControlResponse connect_input_channel_to_track(int /*track_id*/, int /*track_channel*/, int /*input_channel*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse connect_output_channel_to_track(int /*track_id*/, int /*track_channel*/, int /*output_channel*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_input(int /*track_id*/, int /*track_channel*/, int /*input_channel*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_output(int /*track_id*/, int /*track_channel*/, int /*output_channel*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_all_inputs_from_track(int /*track_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_all_outputs_from_track(int /*track_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n};\n\nclass CvGateControllerMockup : public CvGateController, public TestableController\n{\npublic:\n    int get_cv_input_ports() const override {return 0;}\n\n    int get_cv_output_ports() const override {return 0;}\n\n    std::vector<CvConnection> get_all_cv_input_connections() const override\n    {\n        return std::vector<CvConnection>();\n    }\n\n    std::vector<CvConnection> get_all_cv_output_connections() const override\n    {\n        return std::vector<CvConnection>();\n    }\n\n    std::vector<GateConnection> get_all_gate_input_connections() const override\n    {\n        return std::vector<GateConnection>();\n    }\n\n    std::vector<GateConnection> get_all_gate_output_connections() const override\n    {\n        return std::vector<GateConnection>();\n    }\n\n    std::pair<ControlStatus, std::vector<CvConnection>>\n    get_cv_input_connections_for_processor(int /*processor_id*/) const override\n    {\n        return {_return_status, std::vector<CvConnection>()};\n    }\n\n    std::pair<ControlStatus, std::vector<CvConnection>>\n    get_cv_output_connections_for_processor(int /*processor_id*/) const override\n    {\n        return {_return_status, std::vector<CvConnection>()};\n    }\n\n    std::pair<ControlStatus, std::vector<GateConnection>>\n    get_gate_input_connections_for_processor(int /*processor_id*/) const override\n    {\n        return {_return_status, std::vector<GateConnection>()};\n    }\n\n    std::pair<ControlStatus, std::vector<GateConnection>>\n    get_gate_output_connections_for_processor(int /*processor_id*/) const override\n    {\n        return {_return_status, std::vector<GateConnection>()};\n    }\n\n    ControlResponse connect_cv_input_to_parameter(int /*processor_id*/, int /*parameter_id*/, int /*cv_input_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse connect_cv_output_from_parameter(int /*processor_id*/, int /*parameter_id*/, int /*cv_output_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse connect_gate_input_to_processor(int /*processor_id*/, int /*gate_input_id*/, int /*channel*/, int /*note_no*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse connect_gate_output_from_processor(int /*processor_id*/, int /*gate_output_id*/, int /*channel*/, int /*note_no*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_cv_input(int /*processor_id*/, int /*parameter_id*/, int /*cv_input_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_cv_output(int /*processor_id*/, int /*parameter_id*/, int /*cv_output_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_gate_input(int /*processor_id*/, int /*gate_input_id*/, int /*channel*/, int /*note_no*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_gate_output(int /*processor_id*/, int /*gate_output_id*/, int /*channel*/, int /*note_no*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_all_cv_inputs_from_processor(int /*processor_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_all_cv_outputs_from_processor(int /*processor_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_all_gate_inputs_from_processor(int /*processor_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disconnect_all_gate_outputs_from_processor(int /*processor_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n};\n\nclass OscControllerMockup : public OscController, public TestableController\n{\npublic:\n    std::string get_send_ip() const override {return \"\";}\n\n    int get_send_port() const override {return 0;}\n\n    int get_receive_port() const override {return 0;}\n\n    std::vector<std::string> get_enabled_parameter_outputs() const override\n    {\n        return std::vector<std::string>();\n    }\n\n    ControlResponse enable_output_for_parameter(int /*processor_id*/, int /*parameter_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disable_output_for_parameter(int /*processor_id*/, int /*parameter_id*/) override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse enable_all_output() override\n    {\n        return {_return_status, _return_id};\n    }\n\n    ControlResponse disable_all_output() override\n    {\n        return {_return_status, _return_id};\n    }\n};\n\nclass SessionControllerMockup : public SessionController, public TestableController\n{\npublic:\n    control::SessionState save_session() const override\n    {\n        return SessionState();\n    }\n\n    control::ControlResponse restore_session(const control::SessionState& /*state*/) override\n    {\n        return {ControlStatus::UNSUPPORTED_OPERATION, 0};\n    }\n};\n\n\nclass ControlMockup : public SushiControl\n{\npublic:\n    ControlMockup() : SushiControl(&_system_controller_mockup,\n                                   &_transport_controller_mockup,\n                                   &_timing_controller_mockup,\n                                   &_keyboard_controller_mockup,\n                                   &_audio_graph_controller_mockup,\n                                   &_program_controller_mockup,\n                                   &_parameter_controller_mockup,\n                                   &_midi_controller_mockup,\n                                   &_audio_routing_controller_mockup,\n                                   &_cv_gate_controller_mockup,\n                                   &_osc_controller_mockup,\n                                   &_session_controller_mockup) {}\n\n    ControlStatus subscribe_to_notifications(NotificationType /* type */, ControlListener* /* listener */) override\n    {\n        return ControlStatus::OK;\n    }\n\n    bool was_recently_called()\n    {\n        for (auto c : _controllers)\n        {\n            if (c->was_recently_called())\n            {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    void clear_recent_call()\n    {\n        for (auto c : _controllers)\n        {\n            c->clear_recent_call();\n        }\n    }\n\n    SystemControllerMockup* system_controller_mockup()\n    {\n        return &_system_controller_mockup;\n    }\n\n    TransportControllerMockup* transport_controller_mockup()\n    {\n        return &_transport_controller_mockup;\n    }\n\n    TimingControllerMockup* timing_controller_mockup()\n    {\n        return &_timing_controller_mockup;\n    }\n\n    KeyboardControllerMockup* keyboard_controller_mockup()\n    {\n        return &_keyboard_controller_mockup;\n    }\n\n    AudioGraphControllerMockup* audio_graph_controller_mockup()\n    {\n        return &_audio_graph_controller_mockup;\n    }\n\n    ProgramControllerMockup* program_controller_mockup()\n    {\n        return &_program_controller_mockup;\n    }\n\n    ParameterControllerMockup* parameter_controller_mockup()\n    {\n        return &_parameter_controller_mockup;\n    }\n\n    MidiControllerMockup* midi_controller_mockup()\n    {\n        return &_midi_controller_mockup;\n    }\n\n    AudioRoutingControllerMockup* audio_routing_controller_mockup()\n    {\n        return &_audio_routing_controller_mockup;\n    }\n\n    CvGateControllerMockup* cv_gate_controller_mockup()\n    {\n        return &_cv_gate_controller_mockup;\n    }\n\n    OscControllerMockup* osc_controller_mockup()\n    {\n        return &_osc_controller_mockup;\n    }\n\n    SessionControllerMockup* session_controller_mockup()\n    {\n        return &_session_controller_mockup;\n    }\n\nprivate:\n\n    SystemControllerMockup       _system_controller_mockup;\n    TransportControllerMockup    _transport_controller_mockup;\n    TimingControllerMockup       _timing_controller_mockup;\n    KeyboardControllerMockup     _keyboard_controller_mockup;\n    AudioGraphControllerMockup   _audio_graph_controller_mockup;\n    ProgramControllerMockup      _program_controller_mockup;\n    ParameterControllerMockup    _parameter_controller_mockup;\n    MidiControllerMockup         _midi_controller_mockup;\n    AudioRoutingControllerMockup _audio_routing_controller_mockup;\n    CvGateControllerMockup       _cv_gate_controller_mockup;\n    OscControllerMockup          _osc_controller_mockup;\n    SessionControllerMockup      _session_controller_mockup;\n\n    std::vector<TestableController*> _controllers{&_system_controller_mockup,\n                                                  &_transport_controller_mockup,\n                                                  &_timing_controller_mockup,\n                                                  &_keyboard_controller_mockup,\n                                                  &_audio_graph_controller_mockup,\n                                                  &_program_controller_mockup,\n                                                  &_parameter_controller_mockup,\n                                                  &_midi_controller_mockup,\n                                                  &_audio_routing_controller_mockup,\n                                                  &_cv_gate_controller_mockup,\n                                                  &_osc_controller_mockup,\n                                                  &_session_controller_mockup};\n};\n\n} // end namespace sushi::control\n\n#endif //SUSHI_CONTROL_MOCKUP_H\n"
  },
  {
    "path": "test/unittests/test_utils/dummy_processor.h",
    "content": "#ifndef SUSHI_DUMMY_PROCESSOR_H\n#define SUSHI_DUMMY_PROCESSOR_H\n\n#include \"library/processor.h\"\n\nusing namespace sushi::internal;\n\nclass DummyProcessor : public Processor\n{\npublic:\n    explicit DummyProcessor(HostControl host_control) : Processor(host_control)\n    {\n        _max_input_channels = 2;\n        _max_output_channels = 2;\n        _current_input_channels = _max_input_channels;\n        _current_output_channels = _max_output_channels;\n        set_name(\"processor\");\n        this->register_parameter(new ParameterDescriptor(\"param 1\", \"param 1\", \"\", ParameterType::FLOAT));\n        this->register_parameter(new ParameterDescriptor(\"gain\", \"gain\", \"\", ParameterType::FLOAT));\n    }\n\n    ProcessorReturnCode init(float /* sample_rate */) override\n    {\n        return ProcessorReturnCode::OK;\n    }\n\n    void process_event(const RtEvent& /*event*/) override {}\n    void process_audio(const sushi::ChunkSampleBuffer& in_buffer, sushi::ChunkSampleBuffer& out_buffer) override\n    {\n        out_buffer = in_buffer;\n    }\n};\n\nclass DummyMonoProcessor : public DummyProcessor\n{\npublic:\n    explicit DummyMonoProcessor(HostControl host_control) :DummyProcessor(host_control)\n    {\n        _max_input_channels = 1;\n        _max_output_channels = 1;\n        _current_input_channels = _max_input_channels;\n        _current_output_channels = _max_output_channels;\n    }\n};\n\n#endif //SUSHI_DUMMY_PROCESSOR_H\n"
  },
  {
    "path": "test/unittests/test_utils/engine_mockup.h",
    "content": "#ifndef SUSHI_ENGINE_MOCKUP_H\n#define SUSHI_ENGINE_MOCKUP_H\n\n#include <memory>\n\n#include \"engine/base_engine.h\"\n#include \"engine/base_event_dispatcher.h\"\n#include \"dummy_processor.h\"\n#include \"engine/midi_dispatcher.h\"\n#include \"engine/controller/completion_sender.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::midi;\nusing namespace sushi::internal::engine;\nusing namespace sushi::internal::midi_dispatcher;\n\n// Dummy event dispatcher\nclass EventDispatcherMockup : public dispatcher::BaseEventDispatcher, public engine::CompletionSender\n{\npublic:\n    enum class Action {\n        Discard,\n        Execute\n    };\n\n    ~EventDispatcherMockup() override\n    {\n        while (!_queue.empty())\n        {\n            // unique_ptr's will delete content as they go out of scope.\n            _queue.pop_back();\n        }\n    }\n\n    void run() override {}\n    void stop() override {}\n\n    void set_sample_rate(float /*sample_rate*/) override {}\n    void set_time(Time /*timestamp*/) override {}\n\n    int dispatch(std::unique_ptr<Event> /*event*/) override\n    {\n        return EventStatus::HANDLED_OK;\n    }\n\n    void post_event(std::unique_ptr<Event> event) override\n    {\n        _queue.push_front(std::move(event));\n    }\n\n    /**\n     * Call this to check if an event was received, and then discard it.\n     * @return\n     */\n    bool got_event()\n    {\n        if (_queue.empty())\n        {\n            return false;\n        }\n        else\n        {\n            // unique_ptr's will delete content as they go out of scope.\n            _queue.pop_back();\n            return true;\n        }\n    }\n\n    /**\n     * Call this to check if an engine event was received, execute it, and then discard it.\n     * Non-engine events are discarded, and only the first engine event found is executed.\n     * @param engine reference for the events execute method.\n     * @return The execution status of the event.\n     */\n    int execute_engine_event(BaseEngine* engine)\n    {\n        EventStatus::EventStatus status = EventStatus::NOT_HANDLED;\n\n        while (!_queue.empty())\n        {\n            auto event = _queue.back().get();\n\n            // There can be notification events before, which we want to ignore when mocking.\n            if (event->is_engine_event())\n            {\n                /* TODO: If we go with Lambdas in all executable events,\n                 *  the engine can just be captured in the Lambda.\n                 *  If not, it should be a parameter to the Event class constructor.\n                 */\n                auto typed_event = static_cast<EngineEvent*>(event);\n\n                status = static_cast<EventStatus::EventStatus>(typed_event->execute(engine));\n\n                _queue.pop_back();\n\n                return status;\n            }\n            else\n            {\n                _queue.pop_back();\n            }\n        }\n\n        return status;\n    }\n\n    std::unique_ptr<Event> retrieve_event()\n    {\n        if (_queue.empty())\n        {\n            return nullptr;\n        }\n        else\n        {\n            auto e = std::move(_queue.back());\n            _queue.pop_back();\n            return e;\n        }\n    }\n\n    int send_with_completion_notification([[maybe_unused]] std::unique_ptr<Event> event) override\n    {\n        int id = event->id();\n        post_event(std::move(event));\n        return id;\n    }\n\nprivate:\n    std::deque<std::unique_ptr<Event>> _queue;\n};\n\nclass ProcessorContainerMockup : public BaseProcessorContainer\n{\npublic:\n    ProcessorContainerMockup() : _processor(std::make_shared<DummyProcessor>(HostControl(nullptr, nullptr, nullptr))),\n                                 _track(std::make_shared<Track>(HostControl(nullptr, nullptr, nullptr), 2, nullptr)) {}\n\n    bool add_processor(std::shared_ptr<Processor> /*processor*/) override {return true;}\n\n    bool add_track(std::shared_ptr<Track> /*track*/) override {return true;}\n\n    bool remove_processor(ObjectId /*id*/) override {return true;}\n\n    bool remove_track(ObjectId /*track_id*/) override {return true;}\n\n    bool add_to_track(std::shared_ptr<Processor> /*processor*/, ObjectId /*track_id*/, std::optional<ObjectId> /*before_id*/) override {return true;}\n\n    bool remove_from_track(ObjectId /*processor_id*/, ObjectId /*track_id*/) override {return true;}\n\n    bool processor_exists(ObjectId /*id*/) const override {return true;}\n\n    bool processor_exists(const std::string& /*name*/) const override {return true;}\n\n    std::vector<std::shared_ptr<const Processor>> all_processors() const override {return {_processor};}\n\n    std::shared_ptr<Processor> mutable_processor(ObjectId /*id*/) const override {return _processor;}\n\n    std::shared_ptr<Processor> mutable_processor(const std::string& /*name*/) const override {return _processor;}\n\n    std::shared_ptr<const Processor> processor(ObjectId /*id*/) const override {return _processor;}\n\n    std::shared_ptr<const Processor> processor(const std::string& /*name*/) const override {return _processor;}\n\n    std::shared_ptr<Track> mutable_track(ObjectId /*track_id*/) const override {return _track;}\n\n    std::shared_ptr<Track> mutable_track(const std::string& /*track_name*/) const override {return _track;}\n\n    std::shared_ptr<const Track> track(ObjectId /*track_id*/) const override {return _track;}\n\n    std::shared_ptr<const Track> track(const std::string& /*name*/) const override {return _track;}\n\n    std::vector<std::shared_ptr<const Processor>> processors_on_track(ObjectId /*track_id*/) const override {return {_processor};}\n\n    std::vector<std::shared_ptr<const Track>> all_tracks() const override {return {_track};}\n\nprivate:\n    std::shared_ptr<Processor> _processor;\n    std::shared_ptr<Track>     _track;\n};\n\nclass EngineMockup : public BaseEngine\n{\npublic:\n    EngineMockup(float sample_rate) : BaseEngine(sample_rate),\n                                      _transport(sample_rate, &_rt_event_output)\n    {}\n\n    ~EngineMockup() override\n    {}\n\n    void\n    process_chunk(SampleBuffer<AUDIO_CHUNK_SIZE>* in_buffer,\n                  SampleBuffer<AUDIO_CHUNK_SIZE>* out_buffer,\n                  ControlBuffer*, ControlBuffer*, Time, int64_t) override\n    {\n        *out_buffer = *in_buffer;\n        process_called = true;\n    }\n\n    void set_output_latency(Time /*latency*/) override {}\n\n    void set_tempo(float /*tempo*/) override {}\n\n    void set_time_signature(TimeSignature /*signature*/) override {}\n\n    void set_transport_mode(PlayingMode /*mode*/) override {}\n\n    void set_tempo_sync_mode(SyncMode /*mode*/) override {}\n\n    void set_base_plugin_path(const std::string& /*path*/) override {}\n\n    EngineReturnStatus send_rt_event_to_processor(const RtEvent& /*event*/) override\n    {\n        got_rt_event = true;\n        return EngineReturnStatus::OK;\n    }\n\n    dispatcher::BaseEventDispatcher* event_dispatcher() override\n    {\n        return &_event_dispatcher;\n    }\n\n    const BaseProcessorContainer* processor_container() override\n    {\n        return &_processor_container;\n    }\n\n    bool process_called{false};\n    bool got_event{false};\n    bool got_rt_event{false};\n\n    Transport* transport() override\n    {\n        return &_transport;\n    }\n\nprivate:\n    EventDispatcherMockup _event_dispatcher;\n    ProcessorContainerMockup _processor_container;\n\n    Transport _transport;\n    RtEventFifo<10> _rt_event_output;\n};\n\n// TODO: Should this really be here, or is it too specific for the engine_mockup scope,\n//   thus needing its own file?\nclass DummyMidiFrontend : public sushi::internal::midi_frontend::BaseMidiFrontend\n{\npublic:\n    DummyMidiFrontend() : BaseMidiFrontend(nullptr) {}\n\n    ~DummyMidiFrontend() override {}\n\n    bool init() override {return true;}\n    void run()  override {}\n    void stop() override {}\n\n    void send_midi(int input, MidiDataByte /*data*/, Time /*timestamp*/) override\n    {\n        _sent = true;\n        _input = input;\n    }\n\n    bool midi_sent_on_input(int input)\n    {\n        if (_sent && input == _input)\n        {\n            _sent = false;\n            return true;\n        }\n        return false;\n    }\n\nprivate:\n    bool _sent{false};\n    int  _input;\n};\n\n#endif //SUSHI_ENGINE_MOCKUP_H\n"
  },
  {
    "path": "test/unittests/test_utils/event_dispatcher_accessor.h",
    "content": "#ifndef SUSHI_LIBRARY_EVENT_DISPATCHER_ACCESSOR_H\n#define SUSHI_LIBRARY_EVENT_DISPATCHER_ACCESSOR_H\n\n#include \"engine/event_dispatcher.h\"\n#include \"test_utils/engine_mockup.h\"\n\nnamespace sushi::internal::dispatcher\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(EventDispatcher& f) : _friend(f) {}\n\n    void event_loop()\n    {\n        _friend._event_loop();\n    }\n\n    // Not const: it's altered in the test.\n    std::atomic<bool>& running()\n    {\n        return _friend._running;\n    }\n\n    [[nodiscard]] bool parameter_change_queue_empty() const\n    {\n        return _friend._parameter_manager.parameter_change_queue_empty();\n    }\n\n    // Not const: it's altered in the test.\n    EventQueue& in_queue()\n    {\n        return _friend._in_queue;\n    }\n\n    void crank_worker()\n    {\n        _friend._worker._worker();\n    }\n\nprivate:\n    EventDispatcher& _friend;\n};\n\nclass WorkerAccessor\n{\npublic:\n    explicit WorkerAccessor(Worker& f) : _friend(f) {}\n\n    std::atomic<bool>& running()\n    {\n        return _friend._running;\n    }\n\n    void crank_worker()\n    {\n        _friend._worker();\n    }\n\n    EventQueue& queue()\n    {\n        return _friend._queue;\n    }\n\nprivate:\n    Worker& _friend;\n};\n\n}\n\n#endif //SUSHI_LIBRARY_EVENT_DISPATCHER_ACCESSOR_H\n"
  },
  {
    "path": "test/unittests/test_utils/host_control_mockup.h",
    "content": "#ifndef SUSHI_HOST_CONTROL_MOCKUP_H\n#define SUSHI_HOST_CONTROL_MOCKUP_H\n\n#include \"engine/host_control.h\"\n#include \"test_utils/engine_mockup.h\"\n\nconstexpr float DEFAULT_TEST_SAMPLERATE = 44100;\n// Dummy host control object for testing Processors with direct access\n// to a Transport object and a Dummy Event Dispatcher\nclass HostControlMockup\n{\npublic:\n    // Get a HostControl object with dummy dispatcher and transport members\n    HostControl make_host_control_mockup(float sample_rate = DEFAULT_TEST_SAMPLERATE)\n    {\n        _transport.set_sample_rate(sample_rate);\n        return HostControl(&_dummy_dispatcher, &_transport, &_plugin_library);\n    }\n\n    RtEventFifo<10>       _event_output;\n    engine::Transport     _transport{DEFAULT_TEST_SAMPLERATE, &_event_output};\n    engine::PluginLibrary _plugin_library;\n    EventDispatcherMockup _dummy_dispatcher;\n};\n\n#endif //SUSHI_HOST_CONTROL_MOCKUP_H\n"
  },
  {
    "path": "test/unittests/test_utils/jack_mockup.cpp",
    "content": "#include <jack/jack.h>\n#include <jack/midiport.h>\n\n/**\n * @brief Jack mockup that more or less says yes to everything\n */\n\nconstexpr int JACK_NFRAMES = 128;\nconstexpr uint64_t FRAMETIME_64_SMP_44100 = 64 * 1000000 / 48000;\nuint8_t midi_buffer[3] = {0x81, 60, 45};\nfloat buffer[JACK_NFRAMES];\n\nstruct _jack_port\n{\n    int no{0};\n};\n\nstruct _jack_client\n{\n    JackProcessCallback callback_function;\n    void* instance;\n    _jack_port mocked_ports[10];\n};\n\n\njack_client_t * jack_client_open (const char* /*client_name*/,\n                                  jack_options_t /*options*/,\n                                  jack_status_t* status, ...)\n{\n    *status = JackClientZombie; /* I am zombie client! */\n    return new jack_client_t;\n}\n\nint jack_client_close (jack_client_t *client)\n{\n    delete client;\n    return 0;\n}\n\njack_nframes_t jack_get_sample_rate(jack_client_t* /*client*/)\n{\n    return 48000;\n}\n\njack_port_t * jack_port_register (jack_client_t* client,\n                                  const char* /*port_name*/,\n                                  const char* /*port_type*/,\n                                  unsigned long /*flags*/,\n                                  unsigned long /*buffer_size*/)\n{\n    static int i = 0;\n    return &client->mocked_ports[i++];\n}\n\nint jack_set_process_callback (jack_client_t* client,\n                               JackProcessCallback process_callback,\n                               void *arg)\n{\n    client->instance = arg;\n    client->callback_function = process_callback;\n    return 0;\n}\n\nint jack_set_sample_rate_callback (jack_client_t* /*client*/,\n                                   JackSampleRateCallback /*callback*/,\n                                   void* /*arg*/)\n{\n    return 0;\n}\n\n\nint jack_set_latency_callback (jack_client_t* /*client*/,\n                               JackLatencyCallback /*latency_callback*/,\n                               void* /*arg*/)\n{\n    return 0;\n}\n\nint jack_activate (jack_client_t* client)\n{\n    client->callback_function(JACK_NFRAMES, client->instance);\n    return 0;\n}\n\nvoid * jack_port_get_buffer (jack_port_t* /*port*/, jack_nframes_t)\n{\n    return buffer;\n}\n\nuint32_t jack_midi_get_event_count(void* /*port_buffer*/)\n{\n    return 1;\n}\n\nint jack_midi_event_get(jack_midi_event_t *event,\n                        void* /*port_buffer*/,\n                        uint32_t /*event_index*/)\n{\n    event->size = 3;\n    event->buffer = midi_buffer;\n    return 0;\n}\n\n\nint jack_get_cycle_times(const jack_client_t* /*client*/, jack_nframes_t* current_frames,\n                         jack_time_t* current_usecs, jack_time_t* next_usecs, float* period_usecs)\n{\n    *current_frames = 128;\n    *current_usecs = 1000;\n    *next_usecs = 1000 + FRAMETIME_64_SMP_44100;\n    *period_usecs = FRAMETIME_64_SMP_44100;\n    return 0;\n}\n\nvoid jack_port_get_latency_range (jack_port_t* /*port*/, jack_latency_callback_mode_t /*mode*/,\n                                  jack_latency_range_t* range)\n{\n    range->min = 0;\n    range->max = 0;\n    return;\n}\n\n\n/* Functions below are only added for completion, not implemented\n * and shouldn't be called*/\nconst char ** jack_get_ports (jack_client_t* /*client*/,\n                              const char* /*port_name_pattern*/,\n                              const char* /*type_name_pattern*/,\n                              unsigned long /*flags*/)\n{\n    return nullptr;\n}\n\nint jack_connect (jack_client_t* /*client*/,\n                  const char* /*source_port*/,\n                  const char* /*destination_port*/)\n{\n    return 0;\n}\n\nconst char * jack_port_name (const jack_port_t* /*port*/)\n{\n    return nullptr;\n}\n\nvoid jack_free(void* /*ptr*/) {}"
  },
  {
    "path": "test/unittests/test_utils/meta_schema_v4.json",
    "content": "R\"(\n{\n    \"id\": \"http://json-schema.org/draft-04/schema#\",\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n    \"description\": \"Core schema meta-schema\",\n    \"definitions\": {\n        \"schemaArray\": {\n            \"type\": \"array\",\n            \"minItems\": 1,\n            \"items\": { \"$ref\": \"#\" }\n        },\n        \"positiveInteger\": {\n            \"type\": \"integer\",\n            \"minimum\": 0\n        },\n        \"positiveIntegerDefault0\": {\n            \"allOf\": [ { \"$ref\": \"#/definitions/positiveInteger\" }, { \"default\": 0 } ]\n        },\n        \"simpleTypes\": {\n            \"enum\": [ \"array\", \"boolean\", \"integer\", \"null\", \"number\", \"object\", \"string\" ]\n        },\n        \"stringArray\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\" },\n            \"minItems\": 1,\n            \"uniqueItems\": true\n        }\n    },\n    \"type\": \"object\",\n    \"properties\": {\n        \"id\": {\n            \"type\": \"string\"\n        },\n        \"$schema\": {\n            \"type\": \"string\"\n        },\n        \"title\": {\n            \"type\": \"string\"\n        },\n        \"description\": {\n            \"type\": \"string\"\n        },\n        \"default\": {},\n        \"multipleOf\": {\n            \"type\": \"number\",\n            \"minimum\": 0,\n            \"exclusiveMinimum\": true\n        },\n        \"maximum\": {\n            \"type\": \"number\"\n        },\n        \"exclusiveMaximum\": {\n            \"type\": \"boolean\",\n            \"default\": false\n        },\n        \"minimum\": {\n            \"type\": \"number\"\n        },\n        \"exclusiveMinimum\": {\n            \"type\": \"boolean\",\n            \"default\": false\n        },\n        \"maxLength\": { \"$ref\": \"#/definitions/positiveInteger\" },\n        \"minLength\": { \"$ref\": \"#/definitions/positiveIntegerDefault0\" },\n        \"pattern\": {\n            \"type\": \"string\",\n            \"format\": \"regex\"\n        },\n        \"additionalItems\": {\n            \"anyOf\": [\n                { \"type\": \"boolean\" },\n                { \"$ref\": \"#\" }\n            ],\n            \"default\": {}\n        },\n        \"items\": {\n            \"anyOf\": [\n                { \"$ref\": \"#\" },\n                { \"$ref\": \"#/definitions/schemaArray\" }\n            ],\n            \"default\": {}\n        },\n        \"maxItems\": { \"$ref\": \"#/definitions/positiveInteger\" },\n        \"minItems\": { \"$ref\": \"#/definitions/positiveIntegerDefault0\" },\n        \"uniqueItems\": {\n            \"type\": \"boolean\",\n            \"default\": false\n        },\n        \"maxProperties\": { \"$ref\": \"#/definitions/positiveInteger\" },\n        \"minProperties\": { \"$ref\": \"#/definitions/positiveIntegerDefault0\" },\n        \"required\": { \"$ref\": \"#/definitions/stringArray\" },\n        \"additionalProperties\": {\n            \"anyOf\": [\n                { \"type\": \"boolean\" },\n                { \"$ref\": \"#\" }\n            ],\n            \"default\": {}\n        },\n        \"definitions\": {\n            \"type\": \"object\",\n            \"additionalProperties\": { \"$ref\": \"#\" },\n            \"default\": {}\n        },\n        \"properties\": {\n            \"type\": \"object\",\n            \"additionalProperties\": { \"$ref\": \"#\" },\n            \"default\": {}\n        },\n        \"patternProperties\": {\n            \"type\": \"object\",\n            \"additionalProperties\": { \"$ref\": \"#\" },\n            \"default\": {}\n        },\n        \"dependencies\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n                \"anyOf\": [\n                    { \"$ref\": \"#\" },\n                    { \"$ref\": \"#/definitions/stringArray\" }\n                ]\n            }\n        },\n        \"enum\": {\n            \"type\": \"array\",\n            \"minItems\": 1,\n            \"uniqueItems\": true\n        },\n        \"type\": {\n            \"anyOf\": [\n                { \"$ref\": \"#/definitions/simpleTypes\" },\n                {\n                    \"type\": \"array\",\n                    \"items\": { \"$ref\": \"#/definitions/simpleTypes\" },\n                    \"minItems\": 1,\n                    \"uniqueItems\": true\n                }\n            ]\n        },\n        \"format\": { \"type\": \"string\" },\n        \"allOf\": { \"$ref\": \"#/definitions/schemaArray\" },\n        \"anyOf\": { \"$ref\": \"#/definitions/schemaArray\" },\n        \"oneOf\": { \"$ref\": \"#/definitions/schemaArray\" },\n        \"not\": { \"$ref\": \"#\" }\n    },\n    \"dependencies\": {\n        \"exclusiveMaximum\": [ \"maximum\" ],\n        \"exclusiveMinimum\": [ \"minimum\" ]\n    },\n    \"default\": {}\n}\n)\""
  },
  {
    "path": "test/unittests/test_utils/mock_event_dispatcher.h",
    "content": "#ifndef SUSHI_MOCK_EVENT_DISPATCHER_H\n#define SUSHI_MOCK_EVENT_DISPATCHER_H\n\n#include <gmock/gmock.h>\n\n#include \"engine/base_event_dispatcher.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal;\nusing namespace sushi::internal::dispatcher;\n\nclass MockEventDispatcher : public dispatcher::BaseEventDispatcher\n{\npublic:\n    MockEventDispatcher() = default;\n\n    MOCK_METHOD(void,\n                run,\n                (),\n                (override));\n\n    MOCK_METHOD(void,\n                stop,\n                (),\n                (override));\n\n    MOCK_METHOD(void,\n                post_event,\n                (std::unique_ptr<Event>),\n                (override));\n\n    MOCK_METHOD(Status,\n                subscribe_to_keyboard_events,\n                (EventPoster*),\n                (override));\n\n    MOCK_METHOD(Status,\n                subscribe_to_parameter_change_notifications,\n                (EventPoster*),\n                (override));\n\n    MOCK_METHOD(Status,\n                subscribe_to_engine_notifications,\n                (EventPoster*),\n                (override));\n\n    MOCK_METHOD(Status,\n                unsubscribe_from_keyboard_events,\n                (EventPoster*),\n                (override));\n\n    MOCK_METHOD(Status,\n                unsubscribe_from_parameter_change_notifications ,\n                (EventPoster*),\n                (override));\n\n    MOCK_METHOD(Status,\n                unsubscribe_from_engine_notifications,\n                (EventPoster*),\n                (override));\n\n    MOCK_METHOD(void,\n                set_sample_rate,\n                (float),\n                (override));\n\n    MOCK_METHOD(void,\n                set_time,\n                (Time),\n                (override));\n\n    MOCK_METHOD(int,\n                dispatch,\n                (std::unique_ptr<Event> event),\n                (override));\n};\n#endif //SUSHI_MOCK_EVENT_DISPATCHER_H\n"
  },
  {
    "path": "test/unittests/test_utils/mock_midi_frontend.h",
    "content": "/**\n * @brief Mock object for MidiFrontend\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_MOCK_MIDI_FRONTEND_H\n#define SUSHI_MOCK_MIDI_FRONTEND_H\n\n#include <gmock/gmock.h>\n\n#include \"engine/midi_dispatcher.h\"\n\nusing namespace sushi;\n\nclass MockMidiFrontend : public midi_frontend::BaseMidiFrontend\n{\npublic:\n    MockMidiFrontend(midi_receiver::MidiReceiver* receiver) : BaseMidiFrontend(receiver) {}\n\n    MOCK_METHOD(bool,\n                init,\n                (),\n                (override));\n\n    MOCK_METHOD(void,\n                run,\n                (),\n                (override));\n\n    MOCK_METHOD(void,\n                stop,\n                (),\n                (override));\n\n    MOCK_METHOD(void,\n                send_midi,\n                (int, MidiDataByte, Time),\n                (override));\n\n};\n\n#endif //SUSHI_MOCK_MIDI_FRONTEND_H\n"
  },
  {
    "path": "test/unittests/test_utils/mock_osc_interface.h",
    "content": "/*\n* Copyright 2017-2023 Elk Audio AB\n*\n* SUSHI is free software: you can redistribute it and/or modify it under the terms of\n* the GNU Affero General Public License as published by the Free Software Foundation,\n* either version 3 of the License, or (at your option) any later version.\n*\n* SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n* PURPOSE.  See the GNU Affero General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License along with\n* SUSHI. If not, see http://www.gnu.org/licenses/\n*/\n\n#ifndef SUSHI_MOCK_OSC_INTERFACE_H\n#define SUSHI_MOCK_OSC_INTERFACE_H\n\n#include <gmock/gmock.h>\n\n#include \"control_frontends/osc_frontend.h\"\n#include \"control_frontends/osc_utils.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal::osc;\n\nclass MockOscInterface: public BaseOscMessenger\n{\npublic:\n    MockOscInterface(int receive_port,\n                     int send_port,\n                     const std::string& send_ip) : BaseOscMessenger(receive_port,\n                                                                    send_port,\n                                                                    send_ip) {}\n\n    ~MockOscInterface() override = default;\n\n    MOCK_METHOD(bool,\n                init,\n                (),\n                (override));\n\n    MOCK_METHOD(void,\n                run,\n                (),\n                (override));\n\n    MOCK_METHOD(void,\n                stop,\n                (),\n                (override));\n\n    MOCK_METHOD(void*,\n                add_method,\n                (const char* address_pattern,\n                 const char* type_tag_string,\n                 OscMethodType type,\n                 const void* callback_data),\n                (override));\n\n    MOCK_METHOD(void,\n                delete_method,\n                (void* handle),\n                (override));\n\n    MOCK_METHOD(void,\n                send,\n                (const char* address_pattern, float payload),\n                (override));\n\n    MOCK_METHOD(void,\n                send,\n                (const char* address_pattern, int payload),\n                (override));\n\n    MOCK_METHOD(void,\n                send,\n                (const char* address_pattern, const std::string& payload),\n                (override));\n};\n\n#endif // SUSHI_MOCK_OSC_INTERFACE_H\n"
  },
  {
    "path": "test/unittests/test_utils/mock_oscpack.h",
    "content": "/*\n* Copyright 2017-2023 Elk Audio AB\n*\n* SUSHI is free software: you can redistribute it and/or modify it under the terms of\n* the GNU Affero General Public License as published by the Free Software Foundation,\n* either version 3 of the License, or (at your option) any later version.\n*\n* SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n* PURPOSE.  See the GNU Affero General Public License for more details.\n*\n* You should have received a copy of the GNU Affero General Public License along with\n* SUSHI. If not, see http://www.gnu.org/licenses/\n*/\n\n#ifndef SUSHI_MOCK_OSCPACK_H\n#define SUSHI_MOCK_OSCPACK_H\n\n#include <gmock/gmock.h>\n#include <gtest/gtest.h>\n#include <gmock/gmock-actions.h>\n\nclass IpEndpointName\n{\npublic:\n    static constexpr const char* ANY_ADDRESS = \"\"; // Unused - just a placeholder for mocking.\n\n    IpEndpointName(const char* /*addressName*/, int /*port_*/ = 1234) {}\n    virtual ~IpEndpointName() = default;\n};\n\nclass PacketListener\n{\npublic:\n    PacketListener() = default;\n    virtual ~PacketListener() = default;\n};\n\nclass UdpTransmitSocket\n{\npublic:\n    UdpTransmitSocket() = default;\n    explicit UdpTransmitSocket(const IpEndpointName& /*remoteEndpoint*/) {}\n\n    virtual ~UdpTransmitSocket() = default;\n\n    MOCK_METHOD(void,\n                Send,\n                (const char* data, std::size_t size),\n                ());\n};\n\nclass UdpListeningReceiveSocket\n{\npublic:\n    UdpListeningReceiveSocket() = default;\n    UdpListeningReceiveSocket(const IpEndpointName& /*remoteEndpoint*/, PacketListener* /*listener*/)\n    {\n        // This suppresses warnings about calls to AsynchronousBreak.\n        // The method is only called internally from OscPack anyway, so we do not need to\n        // anticipate its invocations in our tests - just mock the sockets to avoid\n        // unit test execution failure caused by unavailable sockets.\n        EXPECT_CALL(*this, AsynchronousBreak()).Times(testing::AnyNumber());\n    }\n\n    virtual ~UdpListeningReceiveSocket() = default;\n\n    MOCK_METHOD(void,\n                Run,\n                (),\n                ());\n\n    MOCK_METHOD(void,\n                AsynchronousBreak,\n                (),\n                ());\n};\n\nnamespace osc\n{\n\nclass ReceivedMessage;\nstruct MessageTerminator;\nstruct BeginMessage;\n\nclass OscPacketListener : public PacketListener\n{\npublic:\n    OscPacketListener() = default;\n    virtual ~OscPacketListener() = default;\n\nprotected:\n    virtual void ProcessMessage(const osc::ReceivedMessage& /*m*/, const IpEndpointName& /*remoteEndpoint*/) = 0;\n};\n\n}\n\n#endif // SUSHI_MOCK_OSCPACK_H\n"
  },
  {
    "path": "test/unittests/test_utils/mock_processor_container.h",
    "content": "/**\n * @brief Mock object for ProcessorContainer\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_MOCK_PROCESSOR_CONTAINER_H\n#define SUSHI_MOCK_PROCESSOR_CONTAINER_H\n\n#include <gmock/gmock.h>\n\n#include \"engine/base_processor_container.h\"\n\nusing namespace sushi;\nusing namespace sushi::internal::engine;\n\nclass MockProcessorContainer : public engine::BaseProcessorContainer\n{\npublic:\n    MOCK_METHOD(bool,\n                add_processor,\n                (std::shared_ptr<Processor>),\n                (override));\n\n    MOCK_METHOD(bool,\n                add_track,\n                (std::shared_ptr<Track>),\n                (override));\n\n    MOCK_METHOD(bool,\n                remove_processor,\n                (ObjectId),\n                (override));\n\n    MOCK_METHOD(bool,\n                remove_track,\n                (ObjectId),\n                (override));\n\n    MOCK_METHOD(bool,\n                add_to_track,\n                (std::shared_ptr<Processor>, ObjectId, std::optional<ObjectId>),\n                (override));\n\n    MOCK_METHOD(bool,\n                remove_from_track,\n                (ObjectId, ObjectId),\n                (override));\n\n    MOCK_METHOD(bool,\n                processor_exists,\n                (ObjectId),\n                (const override));\n\n    MOCK_METHOD(bool,\n                processor_exists,\n                (const std::string&),\n                (const override));\n\n    MOCK_METHOD(std::vector<std::shared_ptr<const Processor>>,\n                all_processors,\n                (),\n                (const override));\n\n    MOCK_METHOD(std::vector<std::shared_ptr<const Processor>>,\n                all_processors,\n                (std::shared_ptr<Track>),\n                (const override));\n\n    MOCK_METHOD(std::shared_ptr<const Processor>,\n                processor,\n                (ObjectId),\n                (const override));\n\n    MOCK_METHOD(std::shared_ptr<const Processor>,\n                processor,\n                (const std::string& ),\n                (const override));\n\n    MOCK_METHOD(std::shared_ptr<Processor>,\n                mutable_processor,\n                (ObjectId),\n                (const override));\n\n    MOCK_METHOD(std::shared_ptr<Processor>,\n                mutable_processor,\n                (const std::string& ),\n                (const override));\n\n    MOCK_METHOD(std::shared_ptr<const Track>,\n                track,\n                (ObjectId),\n                (const override));\n\n    MOCK_METHOD(std::shared_ptr<const Track>,\n                track,\n                (const std::string&),\n                (const override));\n\n    MOCK_METHOD(std::shared_ptr<Track>,\n                mutable_track,\n                (ObjectId),\n                (const override));\n\n    MOCK_METHOD(std::shared_ptr<Track>,\n                mutable_track,\n                (const std::string&),\n                (const override));\n\n    MOCK_METHOD(std::vector<std::shared_ptr<const Processor>>,\n                processors_on_track,\n                (ObjectId),\n                (const override));\n\n    MOCK_METHOD(std::vector<std::shared_ptr<const Track>>,\n                all_tracks,\n                (),\n                (const override));\n\n};\n\n#endif //SUSHI_MOCK_PROCESSOR_CONTAINER_H\n"
  },
  {
    "path": "test/unittests/test_utils/mock_sushi.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n * @brief Mock object for Sushi class\n * @copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_MOCK_SUSHI_H\n#define SUSHI_MOCK_SUSHI_H\n\n#include <gmock/gmock.h>\n\n#include \"sushi.h\"\n\nnamespace sushi\n{\n    void init_logger(const SushiOptions& /*options*/) {}\n    std::string to_string(InitStatus /*init_status*/) { return \"\"; }\n}\n\nusing namespace sushi;\n\nclass MockSushi : public AbstractSushi\n{\npublic:\n    MOCK_METHOD(void,\n                start,\n                (),\n                (override));\n\n    MOCK_METHOD(void,\n                exit,\n                (),\n                (override));\n\n    MOCK_METHOD(engine::Controller*,\n                controller,\n                (),\n                (override));\n\n    MOCK_METHOD(void,\n                set_sample_rate,\n                (float sample_rate),\n                (override));\n\n    MOCK_METHOD(float,\n                sample_rate,\n                (),\n                (const override));\n};\n\n#endif //SUSHI_MOCK_SUSHI_H\n"
  },
  {
    "path": "test/unittests/test_utils/plugin_accessors.h",
    "content": "\n#ifndef SUSHI_LIBRARY_PLUGIN_ACCESSORS_H\n#define SUSHI_LIBRARY_PLUGIN_ACCESSORS_H\n\n#include \"plugins/equalizer_plugin.h\"\n#include \"plugins/gain_plugin.h\"\n#include \"plugins/wav_writer_plugin.h\"\n#include \"plugins/stereo_mixer_plugin.h\"\n\nnamespace sushi::internal::gain_plugin\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(GainPlugin& plugin) : _plugin(plugin) {}\n\n    [[nodiscard]] FloatParameterValue* gain_parameter()\n    {\n        return _plugin._gain_parameter;\n    }\n\nprivate:\n    GainPlugin& _plugin;\n};\n\n}\n\nnamespace sushi::internal::equalizer_plugin\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(const EqualizerPlugin* plugin) : _const_plugin(plugin) {}\n\n    explicit Accessor(EqualizerPlugin* plugin) : _plugin(plugin) {}\n\n    [[nodiscard]] FloatParameterValue* frequency()\n    {\n        return _plugin->_frequency;\n    }\n\n    [[nodiscard]] FloatParameterValue* gain()\n    {\n        return _plugin->_gain;\n    }\n\n    [[nodiscard]] FloatParameterValue* q()\n    {\n        return _plugin->_q;\n    }\n\n    [[nodiscard]] float const_sample_rate() const\n    {\n        return _const_plugin->_sample_rate;\n    }\n\nprivate:\n    EqualizerPlugin* _plugin {nullptr};\n    const EqualizerPlugin* _const_plugin {nullptr};\n};\n\n}\n\nnamespace sushi::internal::stereo_mixer_plugin\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(StereoMixerPlugin& plugin) : _plugin(plugin) {}\n\n    [[nodiscard]] ValueSmootherFilter<float>& ch1_left_gain_smoother()\n    {\n        return _plugin._ch1_left_gain_smoother;\n    }\n\n    [[nodiscard]] ValueSmootherFilter<float>& ch1_right_gain_smoother()\n    {\n        return _plugin._ch1_right_gain_smoother;\n    }\n\n    [[nodiscard]] ValueSmootherFilter<float>& ch2_left_gain_smoother()\n    {\n        return _plugin._ch2_left_gain_smoother;\n    }\n\n    [[nodiscard]] ValueSmootherFilter<float>& ch2_right_gain_smoother()\n    {\n        return _plugin._ch2_right_gain_smoother;\n    }\n\n    [[nodiscard]] FloatParameterValue* ch1_pan()\n    {\n        return _plugin._ch1_pan;\n    }\n\n    [[nodiscard]] FloatParameterValue* ch1_gain()\n    {\n        return _plugin._ch1_gain;\n    }\n\n    [[nodiscard]] FloatParameterValue* ch1_invert_phase()\n    {\n        return _plugin._ch1_invert_phase;\n    }\n\n    [[nodiscard]] FloatParameterValue* ch2_pan()\n    {\n        return _plugin._ch2_pan;\n    }\n\n    [[nodiscard]] FloatParameterValue* ch2_gain()\n    {\n        return _plugin._ch2_gain;\n    }\n\n    [[nodiscard]] FloatParameterValue* ch2_invert_phase()\n    {\n        return _plugin._ch2_invert_phase;\n    }\n\nprivate:\n    StereoMixerPlugin& _plugin;\n};\n\n}\n\nnamespace sushi::internal::wav_writer_plugin\n{\n\nclass Accessor\n{\npublic:\n    explicit Accessor(WavWriterPlugin& plugin) : _plugin(plugin) {}\n\n    [[nodiscard]] BoolParameterValue* recording_parameter()\n    {\n        return _plugin._recording_parameter;\n    }\n\n    WavWriterStatus start_recording()\n    {\n        return _plugin._start_recording();\n    }\n\n    WavWriterStatus stop_recording()\n    {\n        return _plugin._stop_recording();\n    }\n\n    int write_to_file()\n    {\n        return _plugin._write_to_file();\n    }\n\nprivate:\n    WavWriterPlugin& _plugin;\n};\n\n}\n\n#endif //SUSHI_LIBRARY_PLUGIN_ACCESSORS_H\n"
  },
  {
    "path": "test/unittests/test_utils/portaudio_mockup.cpp",
    "content": "#include \"portaudio_mockup.h\"\n\nstd::unique_ptr<MockPortAudio> mockPortAudio;\n\nPaError Pa_Initialize()\n{\n    return mockPortAudio->Pa_Initialize();\n}\n\nPaError Pa_Terminate()\n{\n    return mockPortAudio->Pa_Terminate();\n}\n\nconst char* Pa_GetErrorText(PaError error)\n{\n    return mockPortAudio->Pa_GetErrorText(error);\n}\n\nint Pa_GetDeviceCount()\n{\n    return mockPortAudio->Pa_GetDeviceCount();\n}\n\nint Pa_GetDefaultInputDevice()\n{\n    return mockPortAudio->Pa_GetDefaultInputDevice();\n}\n\nint Pa_GetDefaultOutputDevice()\n{\n    return mockPortAudio->Pa_GetDefaultOutputDevice();\n}\n\nconst PaDeviceInfo* Pa_GetDeviceInfo(int device_index)\n{\n    return mockPortAudio->Pa_GetDeviceInfo(device_index);\n}\n\nconst PaHostApiInfo* Pa_GetHostApiInfo(int api_index)\n{\n    return mockPortAudio->Pa_GetHostApiInfo(api_index);\n}\n\nPaError Pa_IsFormatSupported(const PaStreamParameters* input,\n                             const PaStreamParameters* output,\n                             double samplerate)\n{\n    return mockPortAudio->Pa_IsFormatSupported(input, output, samplerate);\n}\n\nPaTime Pa_GetStreamTime(PaStream* stream)\n{\n    return mockPortAudio->Pa_GetStreamTime(stream);\n}\n\nPaError Pa_IsStreamActive(PaStream* stream)\n{\n    return mockPortAudio->Pa_IsStreamActive(stream);\n}\n\nPaError Pa_OpenStream(PaStream** stream,\n                       const PaStreamParameters *inputParameters,\n                       const PaStreamParameters *outputParameters,\n                       double sampleRate,\n                       unsigned long framesPerBuffer,\n                       PaStreamFlags streamFlags,\n                       PaStreamCallback *streamCallback,\n                       void *userData )\n{\n    return mockPortAudio->Pa_OpenStream(stream,\n                                       inputParameters,\n                                       outputParameters,\n                                       sampleRate,\n                                       framesPerBuffer,\n                                       streamFlags,\n                                       streamCallback,\n                                       userData);\n}\n\nPaError Pa_StartStream(PaStream* stream)\n{\n    return mockPortAudio->Pa_StartStream(stream);\n}\n\nPaError Pa_StopStream(PaStream* stream)\n{\n    return mockPortAudio->Pa_StopStream(stream);\n}\n\nconst PaStreamInfo* Pa_GetStreamInfo(PaStream* stream)\n{\n    return mockPortAudio->Pa_GetStreamInfo(stream);\n}\n"
  },
  {
    "path": "test/unittests/test_utils/portaudio_mockup.h",
    "content": "#include \"gmock/gmock.h\"\n\n// Included for the declarations and data types,\n// used in PortAudioFrontend, which this mock allows testing.\n#include <portaudio.h>\n\n#ifndef SUSHI_TEST_PORTAUDIO_MOCKUP_H\n#define SUSHI_TEST_PORTAUDIO_MOCKUP_H\n\nclass MockPortAudio\n{\npublic:\n    MOCK_METHOD(PaError, Pa_Initialize, ());\n    MOCK_METHOD(PaError, Pa_Terminate, ());\n    MOCK_METHOD(const char*, Pa_GetErrorText, (PaError));\n    MOCK_METHOD(int, Pa_GetDeviceCount, ());\n    MOCK_METHOD(int, Pa_GetDefaultInputDevice, ());\n    MOCK_METHOD(int, Pa_GetDefaultOutputDevice, ());\n    MOCK_METHOD(const PaDeviceInfo*, Pa_GetDeviceInfo, (int));\n    MOCK_METHOD(const PaHostApiInfo*, Pa_GetHostApiInfo, (int));\n    MOCK_METHOD(PaError, Pa_IsFormatSupported, (const PaStreamParameters* input,\n                                                const PaStreamParameters* output,\n                                                double samplerate));\n    MOCK_METHOD(PaTime, Pa_GetStreamTime, (PaStream*));\n    MOCK_METHOD(PaError, Pa_IsStreamActive, (PaStream*));\n    MOCK_METHOD(PaError, Pa_OpenStream, (PaStream** stream,\n                                         const PaStreamParameters *inputParameters,\n                                         const PaStreamParameters *outputParameters,\n                                         double sampleRate,\n                                         unsigned long framesPerBuffer,\n                                         PaStreamFlags streamFlags,\n                                         PaStreamCallback *streamCallback,\n                                         void *userData ));\n    MOCK_METHOD(PaError, Pa_StartStream, (PaStream*));\n    MOCK_METHOD(PaError, Pa_StopStream, (PaStream*));\n    MOCK_METHOD(const PaStreamInfo*, Pa_GetStreamInfo, (PaStream*));\n};\n\nextern std::unique_ptr<MockPortAudio> mockPortAudio;\n\nPaError Pa_Initialize();\n\nPaError Pa_Terminate();\n\nconst char* Pa_GetErrorText(PaError error);\n\nint Pa_GetDeviceCount();\n\nint Pa_GetDefaultInputDevice();\n\nint Pa_GetDefaultOutputDevice();\n\nconst PaDeviceInfo* Pa_GetDeviceInfo(int device_index);\n\nconst PaHostApiInfo* Pa_GetHostApiInfo(int api_index);\n\nPaError Pa_IsFormatSupported(const PaStreamParameters* input,\n                             const PaStreamParameters* output,\n                             double samplerate);\n\nPaTime Pa_GetStreamTime(PaStream* stream);\n\nPaError Pa_IsStreamActive(PaStream* stream);\n\nPaError Pa_OpenStream(PaStream** stream,\n                       const PaStreamParameters *inputParameters,\n                       const PaStreamParameters *outputParameters,\n                       double sampleRate,\n                       unsigned long framesPerBuffer,\n                       PaStreamFlags streamFlags,\n                       PaStreamCallback *streamCallback,\n                       void *userData);\n\nPaError Pa_StartStream(PaStream* stream);\n\nPaError Pa_StopStream(PaStream* stream);\n\nconst PaStreamInfo* Pa_GetStreamInfo(PaStream* stream);\n\n#endif"
  },
  {
    "path": "test/unittests/test_utils/test_utils.h",
    "content": "/**\n * @brief Helper and utility functions for unit tests\n * @Copyright 2017-2023 Elk Audio AB, Stockholm\n */\n\n#ifndef SUSHI_TEST_UTILS_H\n#define SUSHI_TEST_UTILS_H\n\n#include \"sushi/sample_buffer.h\"\n\n#include <iomanip>\n#include <random>\n#include <optional>\n\nusing ::testing::internal::posix::GetEnv;\nusing namespace sushi;\nnamespace test_utils {\n\n// Enough leeway to approximate 6dB to 2 times amplification.\nconst float DECIBEL_ERROR = 0.01f;\n\ntemplate <int size>\ninline void fill_sample_buffer(SampleBuffer<size>& buffer, float value)\n{\n    for (int ch = 0; ch < buffer.channel_count(); ++ch)\n    {\n        std::fill(buffer.channel(ch), buffer.channel(ch) + size, value);\n    }\n}\n\ntemplate <int size>\ninline void fill_buffer_with_noise(SampleBuffer<size>& buffer, std::optional<int> seed=std::nullopt)\n{\n    std::ranlux24 rand_gen;\n    if (seed.has_value())\n    {\n        rand_gen.seed(seed.value());\n    }\n    std::uniform_real_distribution<float> dist{-1.0f, 1.0f};\n\n    for (int ch = 0; ch < buffer.channel_count(); ++ch)\n    {\n        for (int i = 0; i < size; ++i)\n        {\n            buffer.channel(ch)[i] = dist(rand_gen);\n        }\n    }\n}\n\ntemplate <int size>\ninline void assert_buffer_value(float value, const SampleBuffer <size> &buffer)\n{\n    for (int ch = 0; ch < buffer.channel_count(); ++ch)\n    {\n        for (int i = 0; i < size; ++i)\n        {\n            ASSERT_FLOAT_EQ(value, buffer.channel(ch)[i]);\n        }\n    }\n}\n\ntemplate <int size>\ninline void assert_buffer_value(float value, const SampleBuffer<size>& buffer, float error_margin)\n{\n    for (int ch = 0; ch < buffer.channel_count(); ++ch)\n    {\n        for (int i = 0; i < size; ++i)\n        {\n            ASSERT_NEAR(value, buffer.channel(ch)[i], error_margin);\n        }\n    }\n}\n\ntemplate <int size>\ninline void assert_buffer_non_null(const SampleBuffer <size> &buffer)\n{\n    for (int ch = 0; ch < buffer.channel_count(); ++ch)\n    {\n        float sum = 0;\n        for (int i = 0; i < size; ++i)\n        {\n            sum += std::abs(buffer.channel(ch)[i]);\n        }\n        ASSERT_GT(sum, 0.00001);\n    }\n}\n\ntemplate <int size>\ninline void assert_buffer_not_nan(const SampleBuffer <size> &buffer)\n{\n    for (int ch = 0; ch < buffer.channel_count(); ++ch)\n    {\n        for (int i = 0; i < size; ++i)\n        {\n            ASSERT_FALSE(std::isnan(buffer.channel(ch)[i]));\n        }\n    }\n}\n\ninline std::string get_data_dir_path()\n{\n    char const* test_data_dir = GetEnv(\"SUSHI_TEST_DATA_DIR\");\n    if (test_data_dir == nullptr)\n    {\n        EXPECT_TRUE(false) << \"Can't access Test Data environment variable\\n\";\n    }\n    std::string test_config_file(test_data_dir);\n    test_config_file.append(\"/\");\n    return test_config_file;\n}\n\ntemplate <int size>\ninline void compare_buffers(const float static_array[][size], ChunkSampleBuffer& buffer, int channels, float error_margin = 0.0001f)\n{\n    for (int i = 0; i < channels; i++)\n    {\n        for (int j = 0; j < std::min(AUDIO_CHUNK_SIZE, size); j++)\n        {\n            ASSERT_NEAR(static_array[i][j], buffer.channel(i)[j], error_margin);\n        }\n    }\n}\n\ntemplate <int size>\ninline void compare_buffers(ChunkSampleBuffer& buffer_1, ChunkSampleBuffer& buffer_2, int channels, float error_margin = 0.0001f)\n{\n    for (int i = 0; i < channels; i++)\n    {\n        for (int j = 0; j < std::min(AUDIO_CHUNK_SIZE, size); j++)\n        {\n            ASSERT_NEAR(buffer_1.channel(i)[j], buffer_2.channel(i)[j], error_margin);\n        }\n    }\n}\n\n// Utility for creating static buffers such as those used in vst2/lv2_wrapper_test, by copying values from console.\ntemplate <int size>\ninline void print_buffer(ChunkSampleBuffer& buffer, int channels)\n{\n    std::cout << std::scientific << std::setprecision(10);\n    int print_i = 0;\n    for (int i = 0; i < channels; i++)\n    {\n        for (int j = 0; j < std::min(AUDIO_CHUNK_SIZE, size); j++)\n        {\n            std::cout << buffer.channel(i)[j] << \"f, \";\n            print_i++;\n\n\n            if(print_i == 4)\n            {\n                std::cout << std::endl;\n                print_i = 0;\n            }\n        }\n\n        std::cout << std::endl;\n    }\n}\n\n\n// Macro to hide unused variable warnings when using structured bindings\n#define DECLARE_UNUSED(var) [[maybe_unused]] auto unused_##var = var\n\n} // namespace test_utils\n\n\n#endif //SUSHI_TEST_UTILS_H\n"
  },
  {
    "path": "test/unittests/test_utils/track_accessor.h",
    "content": "#ifndef SUSHI_LIBRARY_TRACK_ACCESSOR_H\n#define SUSHI_LIBRARY_TRACK_ACCESSOR_H\n\n#include \"engine/track.h\"\n\nnamespace sushi::internal::engine {\n\nclass TrackAccessor\n{\npublic:\n    explicit TrackAccessor(Track& plugin) : _plugin(plugin) {}\n\n    [[nodiscard]] const std::vector<Processor*>& processors()\n    {\n        return _plugin._processors;\n    }\n\nprivate:\n    Track& _plugin;\n};\n\n}\n\n#endif //SUSHI_LIBRARY_TRACK_ACCESSOR_H\n"
  },
  {
    "path": "test/unittests/test_utils/vst2_test_plugin.cpp",
    "content": "#include <cmath>\n#include <cstdint>\n#include <numbers>\n\n#include \"vst2_test_plugin.h\"\n\nAudioEffect* createEffectInstance(audioMasterCallback audioMaster)\n{\n    return new test_utils::Vst2TestPlugin(audioMaster);\n}\n\nnamespace test_utils {\n\nconstexpr char EFFECT_NAME[] = \"Test Plugin\";\nconstexpr char VENDOR_NAME[] = \"Elk\";\n\nconstexpr char PROGRAM_1[] = \"Program 1\";\nconstexpr char PROGRAM_2[] = \"Program 2\";\nconstexpr char PROGRAM_3[] = \"Program 3\";\nconstexpr std::array<const char*, 3> PROGRAM_NAMES = {PROGRAM_1, PROGRAM_2, PROGRAM_3};\n\nconstexpr char PARAM_1[] = \"Gain\";\nconstexpr char PARAM_2[] = \"Dummy\";\nconstexpr std::array<const char*, 2> PARAM_NAMES = {PARAM_1, PARAM_2};\n\nconstexpr uint8_t NOTE_OFF_PREFIX   = 0b10000000;\nconstexpr uint8_t NOTE_ON_PREFIX    = 0b10010000;\n\nVst2TestPlugin::Vst2TestPlugin(audioMasterCallback audioMaster) : AudioEffectX(audioMaster,\n                                                                               PROGRAM_NAMES.size(),\n                                                                               PARAM_NAMES.size()),\n                                                                  _parameters{{1.0f, 1.0f}},\n                                                                  _program_no{0},\n                                                                  _frequency{0.01},\n                                                                  _phase{0},\n                                                                  _playing{false}\n{\n    setNumInputs(2);\n    setNumOutputs(2);\n    setUniqueID(1234);\n    canProcessReplacing();\n    isSynth(true);\n    programsAreChunks(false);\n}\n\nVst2TestPlugin::~Vst2TestPlugin() = default;\n\nvoid Vst2TestPlugin::setSampleRate(float sampleRate)\n{\n    _samplerate = sampleRate;\n}\n\nvoid Vst2TestPlugin::setParameter(VstInt32 index, float value)\n{\n    if (index < Parameters::MAX_PARAMS)\n    {\n        _parameters[index] = value;\n    }\n}\n\nfloat Vst2TestPlugin::getParameter(VstInt32 index)\n{\n    if (index < Parameters::MAX_PARAMS)\n    {\n        return _parameters[index];\n    }\n    return 0;\n}\n\nvoid Vst2TestPlugin::getParameterName(VstInt32 index, char* label)\n{\n    if (index < Parameters::MAX_PARAMS && index >= 0)\n    {\n        vst_strncpy(label, PARAM_NAMES[index], kVstMaxParamStrLen);\n    }\n}\n\nvoid Vst2TestPlugin::getParameterDisplay(VstInt32 index, char* text)\n{\n    switch (index)\n    {\n        case Parameters::GAIN:\n            dB2string(_parameters[Parameters::GAIN], text, kVstMaxParamStrLen);\n            break;\n\n        case Parameters::DUMMY:\n            float2string(_parameters[Parameters::DUMMY], text, kVstMaxParamStrLen);\n            break;\n\n        default:\n            vst_strncpy(text, \"\", kVstMaxParamStrLen);\n    }\n}\n\nvoid Vst2TestPlugin::getParameterLabel(VstInt32 index, char*label)\n{\n    switch (index)\n    {\n        case Parameters::GAIN:\n            vst_strncpy(label, \"dB\", kVstMaxParamStrLen);\n            break;\n\n        default:\n            vst_strncpy(label, \"\", kVstMaxParamStrLen);\n    }\n}\n\nbool Vst2TestPlugin::getEffectName(char*name)\n{\n    vst_strncpy(name, EFFECT_NAME, kVstMaxEffectNameLen);\n    return true;\n}\n\nbool Vst2TestPlugin::getProductString(char*text)\n{\n    vst_strncpy(text, EFFECT_NAME, kVstMaxProductStrLen);\n    return true;\n}\n\nbool Vst2TestPlugin::getVendorString(char*text)\n{\n    vst_strncpy(text, VENDOR_NAME, kVstMaxVendorStrLen);\n    return true;\n}\n\nVstInt32 Vst2TestPlugin::getVendorVersion()\n{\n    return 1234;\n}\n\nvoid Vst2TestPlugin::processReplacing(float**inputs, float**outputs, VstInt32 sampleFrames)\n{\n    float*in1 = inputs[0];\n    float*in2 = inputs[1];\n    float*out1 = outputs[0];\n    float*out2 = outputs[1];\n\n    for (int i = 0; i < sampleFrames; ++i)\n    {\n        out1[i] = in1[i] * _parameters[Parameters::GAIN];\n        out2[i] = in2[i] * _parameters[Parameters::GAIN];\n        if (_playing)\n        {\n            out1[i] += std::cos(_phase * std::numbers::pi_v<float>);\n            out2[i] += std::signbit(_phase - 0.5f)? 0.5f : -0.5f;\n        }\n        _phase = std::fmod(_phase + _frequency, 1.0f);\n    }\n}\n\nvoid Vst2TestPlugin::setProgram(VstInt32 program)\n{\n    if (program < static_cast<int>(PROGRAM_NAMES.size()) && program >= 0)\n    {\n        _program_no = program;\n    }\n}\n\nVstInt32 Vst2TestPlugin::getProgram()\n{\n    return _program_no;\n}\n\nvoid Vst2TestPlugin::getProgramName(char* name)\n{\n    vst_strncpy(name, PROGRAM_NAMES[_program_no], kVstMaxProgNameLen);\n}\n\nbool Vst2TestPlugin::getProgramNameIndexed(VstInt32 /*category*/, VstInt32 index, char* text)\n{\n    if (index < static_cast<int>(PROGRAM_NAMES.size()) && index >= 0)\n    {\n        vst_strncpy(text, PROGRAM_NAMES[index], kVstMaxProgNameLen);\n        return true;\n    }\n    return false;\n}\n\nVstInt32 Vst2TestPlugin::processEvents(VstEvents* events)\n{\n    for (int i = 0 ; i < events->numEvents; ++i)\n    {\n        if (events->events[i]->type == kVstMidiType)\n        {\n            auto midi_ev = reinterpret_cast<VstMidiEvent*>(events->events[i]);\n            uint8_t command = midi_ev->midiData[0] & 0xf0;\n            if (command == NOTE_ON_PREFIX)\n            {\n                _frequency = 440.0f * std::pow(2.0f, (midi_ev->midiData[1] - 69) / 12.0f) / _samplerate;\n                _playing = true;\n            }\n            if (command == NOTE_OFF_PREFIX)\n            {\n                _playing = false;\n            }\n\n        }\n    }\n    return 0;\n}\n\n}"
  },
  {
    "path": "test/unittests/test_utils/vst2_test_plugin.def",
    "content": "EXPORTS\n    VSTPluginMain\n    main=VSTPluginMain\n"
  },
  {
    "path": "test/unittests/test_utils/vst2_test_plugin.h",
    "content": "#ifndef SUSHI_VST2_TEST_PLUGIN_H\n#define SUSHI_VST2_TEST_PLUGIN_H\n\n#include <array>\n\n#include \"public.sdk/source/vst2.x/audioeffectx.h\"\n\n/**\n * @brief Vst 2.4 plugin for testing the wrapper implementation. It can work as both as\n * a simple gain control, and as a crude synthesizer, outputting a sine wave on the left\n * channel and a square wave on the right.\n */\n\nnamespace test_utils {\n\nenum Parameters\n{\n    GAIN,\n    DUMMY,\n    MAX_PARAMS\n};\n\nclass Vst2TestPlugin : public AudioEffectX\n{\npublic:\n    explicit Vst2TestPlugin(audioMasterCallback audioMaster);\n\n    virtual ~Vst2TestPlugin();\n\n    VstInt32 processEvents (VstEvents* events) override;\n\n    void processReplacing(float**inputs, float**outputs, VstInt32 sampleFrames) override;\n\n    void setSampleRate (float sampleRate) override;\n\n    void setParameter(VstInt32 index, float value) override;\n\n    float getParameter(VstInt32 index) override;\n\n    void getParameterLabel(VstInt32 index, char*label) override;\n\n    void getParameterDisplay(VstInt32 index, char*text) override;\n\n    void getParameterName(VstInt32 index, char*text) override;\n\n    void setProgram(VstInt32 program) override;\n\n    VstInt32 getProgram() override;\n\n    void getProgramName(char* name) override;\n\n    bool getProgramNameIndexed(VstInt32 category, VstInt32 index, char* text) override;\n\n    bool getEffectName(char*name) override;\n\n    bool getVendorString(char*text) override;\n\n    bool getProductString(char*text) override;\n\n    VstInt32 getVendorVersion() override;\n\nprivate:\n    std::array<float, Parameters::MAX_PARAMS> _parameters;\n    int _program_no;\n    float _samplerate;\n    float _frequency;\n    float _phase;\n    bool _playing;\n};\n\n}// namespace test_utils\n\n#endif //SUSHI_VST2_TEST_PLUGIN_H\n"
  },
  {
    "path": "test/unittests/test_utils/vst3_test_plugin.cpp",
    "content": "#ifdef SUSHI_BUILD_WITH_VST3\n\n#include \"vst3_test_plugin.h\"\n#include \"library/vst3x/vst3x_utils.h\"\n#include \"public.sdk/source/vst/vstsinglecomponenteffect.cpp\"\n#include \"public.sdk/source/vst/utility/stringconvert.h\"\n\nusing namespace Steinberg;\nusing namespace Steinberg::Vst;\nusing namespace sushi::internal::vst3;\nnamespace test_utils {\n\nVst3TestPlugin::Vst3TestPlugin()\n{\n    elk::PropertyInfo info;\n    info.id = PROPERTY_ID_1;\n    VST3::StringConvert::convert(\"property_1\", info.name);\n    VST3::StringConvert::convert(\"Property 1\", info.label);\n    info.flags = elk::PropertyInfo::kAudioThreadNotify;\n    _string_properties[0] = info;\n\n    info.id = PROPERTY_ID_2;\n    VST3::StringConvert::convert(\"property_2\", info.name);\n    VST3::StringConvert::convert(\"Property 2\", info.label);\n    info.flags = elk::PropertyInfo::kIsReadOnly;\n    _string_properties[1] = info;\n}\n\nSteinberg::int32 test_callback(void* data, Steinberg::uint16 id)\n{\n    return CALLBACK_ID + id;\n}\n\nVst3TestPlugin::~Vst3TestPlugin() = default;\n\nSteinberg::tresult Vst3TestPlugin::initialize(Steinberg::FUnknown* context)\n{\n    auto result = SingleComponentEffect::initialize(context);\n    if (result == kResultTrue)\n    {\n        parameters.addParameter (STR16 (\"Bypass\"), nullptr, 1, 0, ParameterInfo::kCanAutomate | ParameterInfo::kIsBypass, 1);\n        parameters.addParameter (STR16 (\"Delay\"), STR16 (\"sec\"), 0, 1, ParameterInfo::kCanAutomate, 2);\n        parameters.addParameter (STR16 (\"Output\"), STR16 (\"\"), 0, 1, ParameterInfo::kIsReadOnly, 3);\n\n        addAudioInput (STR16 (\"AudioInput\"), SpeakerArr::kStereo);\n        addAudioOutput (STR16 (\"AudioOutput\"), SpeakerArr::kStereo);\n    }\n\n    host_app = query_vst_interface<IHostApplication>(context);\n    host_extension = query_vst_interface<elk::IElkHostExtension>(context);\n\n    return kResultTrue;\n}\n\nSteinberg::tresult Vst3TestPlugin::setBusArrangements(Steinberg::Vst::SpeakerArrangement* inputs, Steinberg::int32 numIns, Steinberg::Vst::SpeakerArrangement* outputs, Steinberg::int32 numOuts)\n{\n    return SingleComponentEffect::setBusArrangements(inputs, numIns, outputs, numOuts);\n}\n\nSteinberg::tresult Vst3TestPlugin::setActive(Steinberg::TBool state)\n{\n    return SingleComponentEffect::setActive(state);\n}\n\nSteinberg::tresult Vst3TestPlugin::process(Steinberg::Vst::ProcessData& data)\n{\n    return SingleComponentEffect::process(data);\n}\n\nSteinberg::int32 Vst3TestPlugin::getPropertyCount()\n{\n    return _string_properties.size();\n}\nSteinberg::tresult Vst3TestPlugin::getPropertyInfo(Steinberg::int32 propertyIndex, elk::PropertyInfo& info)\n{\n    if (propertyIndex < _string_properties.size())\n    {\n        info = _string_properties[propertyIndex];\n        return kResultOk;\n    }\n    return kInvalidArgument;\n}\n\nSteinberg::tresult Vst3TestPlugin::getPropertyValue(Steinberg::int32 propertyId, elk::PropertyValue& value)\n{\n    for (int i = 0; i < _string_properties.size(); ++i)\n    {\n        if (propertyId == _string_properties[i].id)\n        {\n            value.value = _property_values[i].c_str();\n            value.length = _property_values[i].size();\n            return kResultOk;\n        }\n    }\n    return kInvalidArgument;\n}\n\nSteinberg::tresult Vst3TestPlugin::setPropertyValue(Steinberg::int32 propertyId, const elk::PropertyValue& value)\n{\n    for (int i = 0; i < _string_properties.size(); ++i)\n    {\n        if (propertyId == _string_properties[i].id)\n        {\n            _property_values[i].clear();\n            _property_values[i].append(value.value, value.length);\n            return kResultOk;\n        }\n    }\n    return kInvalidArgument;\n}\n\nvoid Vst3TestPlugin::propertyValueChanged(Steinberg::int32 propertyId, const elk::PropertyValue& value)\n{\n    _last_changed_property_id = propertyId;\n}\n\nvoid Vst3TestPlugin::asyncWorkCompleted(Steinberg::int32 requestId, Steinberg::int32 requestReturnValue)\n{\n    _last_async_id_received = requestId;\n    _last_async_status = requestReturnValue;\n}\n\nSteinberg::tresult Vst3TestPlugin::queryInterface(const char* iid, void** obj)\n{\n    DEF_INTERFACE (elk::IElkControllerExtension);\n    DEF_INTERFACE (elk::IElkProcessorExtension);\n    return SingleComponentEffect::queryInterface (iid, obj);\n}\n\nSteinberg::tresult Vst3TestPlugin::setComponentHandler(Steinberg::Vst::IComponentHandler* handler)\n{\n    if (EditController::setComponentHandler(handler) == kResultOk)\n    {\n        component_handler = componentHandler.get();\n        component_handler_extension = query_vst_interface<elk::IElkComponentHandlerExtension>(handler);\n    }\n    return kResultOk;\n}\n\nbool Vst3TestPlugin::send_async_work_request()\n{\n    Steinberg::int32 id;\n    auto res = host_extension->requestAsyncWork(test_callback, nullptr, id);\n    _last_async_id = id;\n    if (res == Steinberg::kResultOk)\n    {\n        return true;\n    }\n    return false;\n}\n\n}// namespace test_utils\n\n\n#endif //SUSHI_BUILD_WITH_VST3"
  },
  {
    "path": "test/unittests/test_utils/vst3_test_plugin.h",
    "content": "#ifndef SUSHI_LIBRARY_VST_3_TEST_PLUGIN_H\n#define SUSHI_LIBRARY_VST_3_TEST_PLUGIN_H\n\n#ifdef SUSHI_BUILD_WITH_VST3\n\n#include <array>\n\n#include \"pluginterfaces/base/funknownimpl.h\"\n#include \"public.sdk/source/vst/vstsinglecomponenteffect.h\"\n#include \"elk_vst3_extensions/elk_vst3_extensions.h\"\n\nnamespace test_utils {\n\nconstexpr auto CALLBACK_ID = 101;\n\nclass Vst3TestPlugin : public Steinberg::Vst::SingleComponentEffect, ::elk::IElkControllerExtension, ::elk::IElkProcessorExtension\n{\npublic:\n    Vst3TestPlugin();\n    ~Vst3TestPlugin();\n\n    static constexpr size_t STRING_PROPERTIES = 2;\n    static constexpr int PROPERTY_ID_1 = 5;\n    static constexpr int PROPERTY_ID_2 = 7;\n\n    // From IAudioProcessor\n    Steinberg::tresult PLUGIN_API initialize (Steinberg::FUnknown* context) SMTG_OVERRIDE;\n    Steinberg::tresult PLUGIN_API setBusArrangements (Steinberg::Vst::SpeakerArrangement* inputs, Steinberg::int32 numIns, Steinberg::Vst::SpeakerArrangement* outputs, Steinberg::int32 numOuts) SMTG_OVERRIDE;\n\n    Steinberg::tresult PLUGIN_API setActive (Steinberg::TBool state) SMTG_OVERRIDE;\n    Steinberg::tresult PLUGIN_API process (Steinberg::Vst::ProcessData& data) SMTG_OVERRIDE;\n\n    // From Elk extensions\n    Steinberg::int32 PLUGIN_API getPropertyCount() override;\n\n    Steinberg::tresult PLUGIN_API getPropertyInfo(Steinberg::int32 propertyIndex, elk::PropertyInfo& info) override;\n\n    Steinberg::tresult PLUGIN_API getPropertyValue(Steinberg::int32 propertyId, elk::PropertyValue& value) override;\n\n    Steinberg::tresult PLUGIN_API setPropertyValue(Steinberg::int32 propertyId, const elk::PropertyValue& value) override;\n\n    void PLUGIN_API propertyValueChanged(Steinberg::int32 propertyId, const elk::PropertyValue& value) override;\n\n    void PLUGIN_API asyncWorkCompleted(Steinberg::int32 requestId, Steinberg::int32 requestReturnValue) override;\n\n    Steinberg::tresult PLUGIN_API queryInterface (const Steinberg::TUID iid, void** obj) override;\n\n    Steinberg::tresult PLUGIN_API setComponentHandler (Steinberg::Vst::IComponentHandler* handler) override;\n\n    // This refers the refcount methods to the given implementation\n    REFCOUNT_METHODS (Steinberg::Vst::EditControllerEx1)\n\n    bool send_async_work_request();\n\n    // Everything is public to make it easy for tests to verify that everything works as intended\n    Steinberg::OPtr<Steinberg::Vst::IHostApplication> host_app{nullptr};\n    Steinberg::OPtr<elk::IElkHostExtension> host_extension{nullptr};\n    Steinberg::Vst::IComponentHandler* component_handler{nullptr};\n    Steinberg::OPtr<elk::IElkComponentHandlerExtension> component_handler_extension{nullptr};\n\n\n    std::array<elk::PropertyInfo, STRING_PROPERTIES> _string_properties;\n    std::array<std::string, STRING_PROPERTIES> _property_values;\n    int _last_changed_property_id;\n\n    int _last_async_id;\n    int _last_async_id_received;\n    int _last_async_status;\n};\n\n}// namespace test_utils\n\n#else //SUSHI_BUILD_WITH_VST3\n\nclass Vst3TestPlugin\n{\n}\n\n#endif  //SUSHI_BUILD_WITH_VST3\n#endif //SUSHI_LIBRARY_VST_3_TEST_PLUGIN_H\n"
  },
  {
    "path": "third-party/.gitkeep",
    "content": ""
  },
  {
    "path": "third-party/CMakeLists.txt",
    "content": "add_subdirectory(fifo EXCLUDE_FROM_ALL)\n\nadd_subdirectory(lv2_host)\n\n###########################\n# oscpack compile options #\n###########################\n\nset(BUILD_SHARED_LIBS OFF)\nset(OSC_LIB_ONLY ON)\n\n# Oscpack and vst3sdk require an older cmake version than what cmake 4 supports\n# This relaxes this requirement and allows those libraries to be configured with cmake\nset(CMAKE_POLICY_VERSION_MINIMUM 3.10)\n\nadd_subdirectory(oscpack)\n\nif (MSVC)\n    target_compile_options(oscpack PRIVATE \"/W0\")\nelse()\n    # oscpack sets the -Wall flag, which is negated below, to suppress warnings.\n    target_compile_options(oscpack PRIVATE \"-Wno-all\")\nendif()\n\n##########################\n#  VST3SDK and examples  #\n##########################\nif (${SUSHI_WITH_VST3})\n    set(vstsdk_VERSION 3.8.0)\n\n    # Exclude VstGui, hosting samples and skip validation\n    set(SMTG_ENABLE_VSTGUI_SUPPORT OFF CACHE BOOL \"\" FORCE)\n    set(SMTG_ENABLE_VST3_HOSTING_EXAMPLES OFF CACHE BOOL \"\" FORCE)\n    set(SMTG_RUN_VST_VALIDATOR OFF CACHE BOOL \"\" FORCE)\n    set(SMTG_CREATE_PLUGIN_LINK OFF CACHE BOOL \"\" FORCE)\n\n    # if cross compiling, switch off the create module info post build step\n    if(CMAKE_CROSSCOMPILING)\n        set(SMTG_CREATE_MODULE_INFO OFF CACHE BOOL \"\" FORCE)\n    endif()\n\n    # VST3 example plugins are needed by unit tests\n    set(SMTG_ENABLE_VST3_PLUGIN_EXAMPLES ON CACHE BOOL \"\" FORCE)\n\n    # Don't install example plugins in ~/.vst3, keep them in the build directory\n    set(SMTG_PLUGIN_TARGET_PATH ${CMAKE_CURRENT_BINARY_DIR}/vst3sdk/VST3 CACHE STRING \"\" FORCE)\n    if (${CMAKE_SYSTEM_NAME} STREQUAL \"Linux\")\n        message(\"Vst3 Linux detected\")\n        set(SMTG_LINUX ON)\n    endif()\n\n    if (${CMAKE_SYSTEM_NAME} STREQUAL \"Darwin\")\n        message(\"Vst3 macOS detected\")\n        set(SMTG_MAC ON)\n    endif()\n\n    add_subdirectory(vst3sdk)\n    add_subdirectory(elk-plugin-extensions)\n\n    # vst3sdk overrides SMTG_ENABLE_VST3_PLUGIN_EXAMPLES if the sdk is included as a submodule.\n    # So, we need to manually add adelay, as it is used by sushi unit tests.\n    # We also need to redo some Steinberg initialisation.\n\n    list(APPEND CMAKE_MODULE_PATH \"${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/cmake/modules\")\n    include(SMTG_VST3_SDK)\n\n    smtg_setup_platform_toolset()\n    set(SDK_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk)\n    set(public_sdk_SOURCE_DIR ${SDK_ROOT}/public.sdk)\n    set(pluginterfaces_SOURCE_DIR ${SDK_ROOT}/pluginterfaces)\n    set(VST_SDK TRUE)\n    set(SDK_IDE_PLUGIN_EXAMPLES_FOLDER FOLDER \"PlugInExamples\")\n\n    # Suppress warnings to keep our build output clean\n    if(MSVC)\n        set(VST3_COMPILE_FLAGS \" /W0\")\n    else()\n        set(VST3_COMPILE_FLAGS \"-Wno-unused-parameter \\\n                                -Wno-extra -Wno-deprecated \\\n                                -Wno-cpp \\\n                                -Wno-pointer-bool-conversion \\\n                                -Wno-suggest-override \\\n                                -Wno-deprecated-declarations \\\n                                -Wno-unqualified-std-cast-call \\\n                                -Wno-shorten-64-to-32 \\\n                                -Wformat=0\")\n\n        if (${CMAKE_SYSTEM_NAME} STREQUAL \"Darwin\")\n            set(VST3_COMPILE_FLAGS \"${VST3_COMPILE_FLAGS} -fobjc-arc -Wno-unqualified-std-cast-call\")\n        endif()\n    endif()\n\n    if (SMTG_ENABLE_VST3_PLUGIN_EXAMPLES)\n        set_target_properties(mda-vst3 PROPERTIES COMPILE_FLAGS ${VST3_COMPILE_FLAGS})\n        set_target_properties(base PROPERTIES COMPILE_FLAGS ${VST3_COMPILE_FLAGS})\n        set_target_properties(adelay PROPERTIES COMPILE_FLAGS ${VST3_COMPILE_FLAGS})\n    endif()\n\n    if (MSVC)\n        # don't check the return code as this will fail if the patch is already applied\n        execute_process(\n            COMMAND git apply ${CMAKE_SOURCE_DIR}/third-party/vst3sdk.windows.patch\n            WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\n        )\n    endif()\n\nendif()\n\nset(FREEVERB_UNDENORMALIZE OFF CACHE BOOL \"\" FORCE)\nadd_subdirectory(freeverb)\n\nif (SUSHI_WITH_PORTAUDIO)\n    add_subdirectory(portaudio)\n\n    # ASIO doesn't support unicode\n    if (MSVC)\n        target_compile_options(PortAudio PRIVATE /U_UNICODE /UUNICODE)\n    endif()\nendif()\n#####################################################\n#  External Projects definitions (non-Cmake based)  #\n#####################################################\n\n# Add here custom build steps for third-party libraries\n# that need to be linked statically,\n# example:\n#\n# include(ExternalProject)\n#\n# ExternalProject_Add(static_library\n#         SOURCE_DIR ${PROJECT_SOURCE_DIR}/third-party/static_library\n#         BUILD_IN_SOURCE 1\n#         CONFIGURE_COMMAND ./autogen.sh COMMAND ./configure --prefix=${CMAKE_CURRENT_SOURCE_DIR}/install/static_library\n#         BUILD_COMMAND make\n#         INSTALL_COMMAND make install\n# )\n"
  },
  {
    "path": "third-party/fifo/CMakeLists.txt",
    "content": "add_library(fifo INTERFACE)\ntarget_include_directories(fifo INTERFACE include)"
  },
  {
    "path": "third-party/fifo/include/fifo/circularfifo_memory_relaxed_aquire_release.h",
    "content": "/* Not any company's property but Public-Domain\r\n * Do with source-code as you will. No requirement to keep this\r\n * header if need to use it/change it/ or do whatever with it\r\n *\r\n * Note that there is No guarantee that this code will work\r\n * and I take no responsibility for this code and any problems you\r\n * might get if using it.\r\n *\r\n * Code & platform dependent issues with it was originally\r\n * published at http://www.kjellkod.cc/threadsafecircularqueue\r\n * 2012-16-19  @author Kjell Hedstrom, hedstrom@kjellkod.cc */\r\n\r\n// should be mentioned the thinking of what goes where\r\n// it is a \"controversy\" whether what is tail and what is head\r\n// http://en.wikipedia.org/wiki/FIFO#Head_or_tail_first\r\n\r\n/**\r\n * @brief Fifo implementation that is wait free for 1 consumer/ 1 producer.\r\n */\r\n\r\n#ifndef CIRCULARFIFO_AQUIRE_RELEASE_H_\r\n#define CIRCULARFIFO_AQUIRE_RELEASE_H_\r\n\r\n#include <atomic>\r\n#include <cstddef>\r\n#include <array>\r\n\r\nnamespace memory_relaxed_aquire_release {\r\ntemplate<typename Element, size_t Size> \r\nclass CircularFifo{\r\npublic:\r\n  enum { Capacity = Size+1 };\r\n\r\n  CircularFifo() : _tail(0), _head(0){}\r\n  CircularFifo(const Element& inializer) : _tail(0), _head(0)\r\n  {\r\n    for (auto& el : _array)\r\n    {\r\n      el = inializer;\r\n    }\r\n  }\r\n  virtual ~CircularFifo() {}\r\n\r\n  bool push(const Element& item); // pushByMOve?\r\n  bool pop(Element& item);\r\n\r\n  bool wasEmpty() const;\r\n  bool wasFull() const;\r\n  bool isLockFree() const;\r\n\r\nprivate:\r\n  size_t increment(size_t idx) const; \r\n\r\n  std::atomic <size_t>  _tail;  // tail(input) index\r\n  Element    _array[Capacity];\r\n  std::atomic<size_t>   _head; // head(output) index\r\n};\r\n\r\ntemplate<typename Element, size_t Size>\r\nbool CircularFifo<Element, Size>::push(const Element& item)\r\n{\t\r\n  const auto current_tail = _tail.load(std::memory_order_relaxed); \r\n  const auto next_tail = increment(current_tail); \r\n  if(next_tail != _head.load(std::memory_order_acquire))                           \r\n  {\t\r\n    _array[current_tail] = item;\r\n    _tail.store(next_tail, std::memory_order_release); \r\n    return true;\r\n  }\r\n  \r\n  return false; // full queue\r\n\r\n}\r\n\r\n\r\n// Pop by Consumer can only update the head (load with relaxed, store with release)\r\n//     the tail must be accessed with at least aquire\r\ntemplate<typename Element, size_t Size>\r\nbool CircularFifo<Element, Size>::pop(Element& item)\r\n{\r\n  const auto current_head = _head.load(std::memory_order_relaxed);  \r\n  if(current_head == _tail.load(std::memory_order_acquire)) \r\n    return false; // empty queue\r\n\r\n  item = _array[current_head]; \r\n  _head.store(increment(current_head), std::memory_order_release); \r\n  return true;\r\n}\r\n\r\ntemplate<typename Element, size_t Size>\r\nbool CircularFifo<Element, Size>::wasEmpty() const\r\n{\r\n  // snapshot with acceptance of that this comparison operation is not atomic\r\n  return (_head.load() == _tail.load()); \r\n}\r\n\r\n\r\n// snapshot with acceptance that this comparison is not atomic\r\ntemplate<typename Element, size_t Size>\r\nbool CircularFifo<Element, Size>::wasFull() const\r\n{\r\n  const auto next_tail = increment(_tail.load()); // aquire, we dont know who call\r\n  return (next_tail == _head.load());\r\n}\r\n\r\n\r\ntemplate<typename Element, size_t Size>\r\nbool CircularFifo<Element, Size>::isLockFree() const\r\n{\r\n  return (_tail.is_lock_free() && _head.is_lock_free());\r\n}\r\n\r\ntemplate<typename Element, size_t Size>\r\nsize_t CircularFifo<Element, Size>::increment(size_t idx) const\r\n{\r\n  return (idx + 1) % Capacity;\r\n}\r\n\r\n} // memory_relaxed_aquire_release\r\n#endif /* CIRCULARFIFO_AQUIRE_RELEASE_H_ */\r\n"
  },
  {
    "path": "third-party/lv2_host/CMakeLists.txt",
    "content": "find_package(LV2 CONFIG REQUIRED)\n\nadd_library(lv2_host STATIC src/lv2_evbuf.cpp\n        src/lv2_symap.cpp)\n\ntarget_include_directories(lv2_host PUBLIC include)\n\ntarget_compile_features(lv2_host PRIVATE cxx_std_20)\ntarget_link_libraries(lv2_host PRIVATE lv2::lv2)\n\nif(NOT MSVC)\n    target_compile_options(lv2_host PRIVATE -Wall -Wextra -Wno-psabi -fno-rtti -ffast-math)\nendif ()\n"
  },
  {
    "path": "third-party/lv2_host/include/lv2_host/lv2_evbuf.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n// This file has been copied from the Jalv LV2 plugin host - only minimal refactoring has been carried out.\n\n#ifndef LV2_EVBUF_H\n#define LV2_EVBUF_H\n\n#include <stdint.h>\n\nnamespace lv2_host {\n\nextern \"C\" {\n\n/**\nAn abstract/opaque LV2 event buffer.\n*/\ntypedef struct LV2_Evbuf_Impl LV2_Evbuf;\n\n/**\nAn iterator over an LV2_Evbuf.\n*/\ntypedef struct {\n    LV2_Evbuf *evbuf;\n    uint32_t offset;\n} LV2_Evbuf_Iterator;\n\n/**\nAllocate a new, empty event buffer.\nURIDs for atom:Chunk and atom:Sequence must be passed for LV2_EVBUF_ATOM.\n*/\nLV2_Evbuf* lv2_evbuf_new(uint32_t capacity, uint32_t atom_Chunk, uint32_t atom_Sequence);\n\n/**\nFree an event buffer allocated with lv2_evbuf_new.\n*/\nvoid lv2_evbuf_free(LV2_Evbuf *evbuf);\n\n/**\nClear and initialize an existing event buffer.\nThe contents of buf are ignored entirely and overwritten, except capacity\nwhich is unmodified.\nIf input is false and this is an atom buffer, the buffer will be prepared\nfor writing by the plugin.  This MUST be called before every run cycle.\n*/\nvoid lv2_evbuf_reset(LV2_Evbuf *evbuf, bool input);\n\n/**\nReturn the total padded size of the events stored in the buffer.\n*/\nuint32_t lv2_evbuf_get_size(LV2_Evbuf *evbuf);\n\n/**\nReturn the actual buffer implementation.\nThe format of the buffer returned depends on the buffer type.\n*/\nvoid* lv2_evbuf_get_buffer(LV2_Evbuf *evbuf);\n\n/**\nReturn an iterator to the start of `evbuf`.\n*/\nLV2_Evbuf_Iterator lv2_evbuf_begin(LV2_Evbuf *evbuf);\n\n/**\nReturn an iterator to the end of `evbuf`.\n*/\nLV2_Evbuf_Iterator lv2_evbuf_end(LV2_Evbuf *evbuf);\n\n/**\nCheck if `iter` is valid.\n@return True if `iter` is valid, otherwise false (past end of buffer)\n*/\nbool lv2_evbuf_is_valid(LV2_Evbuf_Iterator iter);\n\n/**\nAdvance `iter` forward one event.\n`iter` must be valid.\n@return True if `iter` is valid, otherwise false (reached end of buffer)\n*/\nLV2_Evbuf_Iterator lv2_evbuf_next(LV2_Evbuf_Iterator iter);\n\n/**\nDereference an event iterator (i.e. get the event currently pointed to).\n`iter` must be valid.\n`type` Set to the type of the event.\n`size` Set to the size of the event.\n`data` Set to the contents of the event.\n@return True on success.\n*/\nbool lv2_evbuf_get(LV2_Evbuf_Iterator iter,\n              uint32_t *frames,\n              uint32_t *subframes,\n              uint32_t *type,\n              uint32_t *size,\n              uint8_t **data);\n\n/**\nWrite an event at `iter`.\nThe event (if any) pointed to by `iter` will be overwritten, and `iter`\nincremented to point to the following event (i.e. several calls to this\nfunction can be done in sequence without twiddling iter in-between).\n@return True if event was written, otherwise false (buffer is full).\n*/\nbool lv2_evbuf_write(LV2_Evbuf_Iterator *iter,\n                uint32_t frames,\n                uint32_t /*subframes*/,\n                uint32_t type,\n                uint32_t size,\n                const uint8_t *data);\n}\n\n}\n\n#endif /* LV2_EVBUF_H */"
  },
  {
    "path": "third-party/lv2_host/include/lv2_host/lv2_symap.h",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n/**\n   @file symap.h API for Symap, a basic symbol map (string interner).\n\n   Particularly useful for implementing LV2 URI mapping.\n\n   @see <a href=\"http://lv2plug.in/ns/ext/urid\">LV2 URID</a>\n*/\n\n// This file has been copied from the Jalv LV2 plugin host example - no refactoring has been carried out\n\n#ifndef SYMAP_H\n#define SYMAP_H\n\n#include <cstdint>\n\nnamespace lv2_host {\n\nstruct SymapImpl;\n\ntypedef struct SymapImpl Symap;\n\n/**\nCreate a new symbol map.\n*/\nSymap* symap_new(void);\n\n/**\nFree a symbol map.\n*/\nvoid symap_free(Symap* map);\n\n/**\nMap a string to a symbol ID if it is already mapped, otherwise return 0.\n*/\nuint32_t symap_try_map(Symap* map, const char* sym);\n\n/**\nMap a string to a symbol ID.\n\nNote that 0 is never a valid symbol ID.\n*/\nuint32_t symap_map(Symap* map, const char* sym);\n\n/**\nUnmap a symbol ID back to a symbol, or NULL if no such ID exists.\n\nNote that 0 is never a valid symbol ID.\n*/\nconst char* symap_unmap(Symap* map, uint32_t id);\n\n}\n\n#endif /* SYMAP_H */"
  },
  {
    "path": "third-party/lv2_host/src/lv2_evbuf.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n// This file has been copied from the Jalv LV2 plugin host example - little refactoring has been carried out\n\n#include <cassert>\n#include <cstdlib>\n#include <cstring>\n\n#include <lv2/atom/atom.h>\n\n#include \"lv2_host/lv2_evbuf.h\"\n\nnamespace lv2_host {\n\nstruct LV2_Evbuf_Impl\n{\n    uint32_t capacity;\n    uint32_t atom_Chunk;\n    uint32_t atom_Sequence;\n    LV2_Atom_Sequence buf;\n};\n\nstatic inline uint32_t lv2_evbuf_pad_size(uint32_t size)\n{\n    return (size + 7) & (~7);\n}\n\nLV2_Evbuf* lv2_evbuf_new(uint32_t capacity, uint32_t atom_Chunk, uint32_t atom_Sequence)\n{\n    // TODO: (inherited from JALV): memory must be 64-bit aligned\n    LV2_Evbuf* evbuf = (LV2_Evbuf*) malloc(sizeof(LV2_Evbuf) + sizeof(LV2_Atom_Sequence) + capacity);\n    evbuf->capacity = capacity;\n    evbuf->atom_Chunk = atom_Chunk;\n    evbuf->atom_Sequence = atom_Sequence;\n    lv2_evbuf_reset(evbuf, true);\n    return evbuf;\n}\n\nvoid lv2_evbuf_free(LV2_Evbuf *evbuf)\n{\n    free(evbuf);\n}\n\nvoid lv2_evbuf_reset(LV2_Evbuf *evbuf, bool input)\n{\n    if (input)\n    {\n        evbuf->buf.atom.size = sizeof(LV2_Atom_Sequence_Body);\n        evbuf->buf.atom.type = evbuf->atom_Sequence;\n    }\n    else\n    {\n        evbuf->buf.atom.size = evbuf->capacity;\n        evbuf->buf.atom.type = evbuf->atom_Chunk;\n    }\n}\n\nuint32_t lv2_evbuf_get_size(LV2_Evbuf *evbuf)\n{\n    assert(evbuf->buf.atom.type != evbuf->atom_Sequence\n           || evbuf->buf.atom.size >= sizeof(LV2_Atom_Sequence_Body));\n    return evbuf->buf.atom.type == evbuf->atom_Sequence\n           ? evbuf->buf.atom.size - sizeof(LV2_Atom_Sequence_Body)\n           : 0;\n}\n\nvoid* lv2_evbuf_get_buffer(LV2_Evbuf *evbuf)\n{\n    return &evbuf->buf;\n}\n\nLV2_Evbuf_Iterator lv2_evbuf_begin(LV2_Evbuf *evbuf)\n{\n    LV2_Evbuf_Iterator iter = {evbuf, 0};\n    return iter;\n}\n\nLV2_Evbuf_Iterator lv2_evbuf_end(LV2_Evbuf *evbuf)\n{\n    const uint32_t size = lv2_evbuf_get_size(evbuf);\n    const LV2_Evbuf_Iterator iter = {evbuf, lv2_evbuf_pad_size(size)};\n    return iter;\n}\n\nbool lv2_evbuf_is_valid(LV2_Evbuf_Iterator iter)\n{\n    return iter.offset < lv2_evbuf_get_size(iter.evbuf);\n}\n\nLV2_Evbuf_Iterator lv2_evbuf_next(LV2_Evbuf_Iterator iter)\n{\n    if (!lv2_evbuf_is_valid(iter))\n    {\n        return iter;\n    }\n\n    LV2_Evbuf *evbuf = iter.evbuf;\n    uint32_t offset = iter.offset;\n    uint32_t size;\n    size = ((LV2_Atom_Event*)\n            ((char*) LV2_ATOM_CONTENTS(LV2_Atom_Sequence, &evbuf->buf.atom)\n             + offset))->body.size;\n    offset += lv2_evbuf_pad_size(sizeof(LV2_Atom_Event) + size);\n\n    LV2_Evbuf_Iterator next = {evbuf, offset};\n    return next;\n}\n\nbool lv2_evbuf_get(LV2_Evbuf_Iterator iter,\n              uint32_t *frames,\n              uint32_t *subframes,\n              uint32_t *type,\n              uint32_t *size,\n              uint8_t **data) {\n    *frames = *subframes = *type = *size = 0;\n    *data = NULL;\n\n    if (!lv2_evbuf_is_valid(iter))\n    {\n        return false;\n    }\n\n    LV2_Atom_Sequence* aseq = &iter.evbuf->buf;\n    LV2_Atom_Event* aev = (LV2_Atom_Event *) (\n            (char*) LV2_ATOM_CONTENTS(LV2_Atom_Sequence, aseq) + iter.offset);\n\n    *frames = aev->time.frames;\n    *subframes = 0;\n    *type = aev->body.type;\n    *size = aev->body.size;\n    *data = (uint8_t *) LV2_ATOM_BODY(&aev->body);\n\n    return true;\n}\n\nbool lv2_evbuf_write(LV2_Evbuf_Iterator *iter,\n                uint32_t frames,\n                uint32_t /*subframes*/,\n                uint32_t type,\n                uint32_t size,\n                const uint8_t *data)\n                {\n    auto aseq = &iter->evbuf->buf;\n\n    if (iter->evbuf->capacity - sizeof(LV2_Atom) - aseq->atom.size <\n        sizeof(LV2_Atom_Event) + size)\n    {\n        return false;\n    }\n\n    auto aev = reinterpret_cast<LV2_Atom_Event*>(static_cast<char*>(LV2_ATOM_CONTENTS(LV2_Atom_Sequence, aseq)) + iter->offset);\n\n    aev->time.frames = frames;\n    aev->body.type = type;\n    aev->body.size = size;\n    memcpy(LV2_ATOM_BODY(&aev->body), data, size);\n\n    size = lv2_evbuf_pad_size(sizeof(LV2_Atom_Event) + size);\n    aseq->atom.size += size;\n    iter->offset += size;\n\n    return true;\n}\n\n}\n"
  },
  {
    "path": "third-party/lv2_host/src/lv2_symap.cpp",
    "content": "/*\n * Copyright 2017-2023 Elk Audio AB\n *\n * SUSHI is free software: you can redistribute it and/or modify it under the terms of\n * the GNU Affero General Public License as published by the Free Software Foundation,\n * either version 3 of the License, or (at your option) any later version.\n *\n * SUSHI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\n * PURPOSE. See the GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License along with\n * SUSHI. If not, see http://www.gnu.org/licenses/\n */\n\n// This file has been copied from the Jalv LV2 plugin host example\n// - no refactoring other than aesthetic has been carried out.\n\n#include <assert.h>\n#include <stdlib.h>\n#include <string.h>\n\n#include \"lv2_host/lv2_symap.h\"\n\n/**\n  @file symap.c Implementation of Symap, a basic symbol map (string interner).\n\n  This implementation is primitive, but has some desirable qualities: good\n  (O(lg(n)) lookup performance for already-mapped symbols, minimal space\n  overhead, extremely fast (O(1)) reverse mapping (ID to string), simple code,\n  no dependencies.\n\n  The tradeoff is that mapping new symbols may be quite slow.  In other words,\n  this implementation is ideal for use cases with a relatively limited set of\n  symbols, or where most symbols are mapped early.  It will not fare so well\n  with very dynamic sets of symbols.  For that, you're better off with a\n  tree-based implementation (and the associated space cost, especially if you\n  need reverse mapping).\n*/\n\nnamespace lv2_host {\n\nstruct SymapImpl\n{\n    /**\n       Unsorted array of strings, such that the symbol for ID i is found\n       at symbols[i - 1].\n    */\n    char **symbols;\n\n    /**\n       Array of IDs, sorted by corresponding string in `symbols`.\n    */\n    uint32_t *index;\n\n    /**\n       Number of symbols (number of items in `symbols` and `index`).\n    */\n    uint32_t size;\n};\n\nSymap* symap_new(void)\n{\n    auto map = (Symap*) malloc(sizeof(Symap));\n    map->symbols = NULL;\n    map->index = NULL;\n    map->size = 0;\n    return map;\n}\n\nvoid symap_free(Symap *map)\n{\n    if (!map)\n    {\n        return;\n    }\n\n    for (uint32_t i = 0; i < map->size; ++i)\n    {\n        free(map->symbols[i]);\n    }\n\n    free(map->symbols);\n    free(map->index);\n    free(map);\n}\n\nstatic char* symap_strdup(const char *str)\n{\n    const size_t len = strlen(str);\n    char *copy = (char *) malloc(len + 1);\n    memcpy(copy, str, len + 1);\n    return copy;\n}\n\n/**\nReturn the index into map->index (not the ID) corresponding to `sym`,\nor the index where a new entry for `sym` should be inserted.\n*/\nstatic uint32_t symap_search(const Symap *map, const char *sym, bool *exact)\n{\n    *exact = false;\n\n    if (map->size == 0)\n    {\n        return 0;  // Empty map, insert at 0\n    }\n    else if (strcmp(map->symbols[map->index[map->size - 1] - 1], sym) < 0)\n    {\n        return map->size;  // Greater than last element, append\n    }\n\n    uint32_t lower = 0;\n    uint32_t upper = map->size - 1;\n    uint32_t i = upper;\n    int cmp;\n\n    while (upper >= lower)\n    {\n        i = lower + ((upper - lower) / 2);\n        cmp = strcmp(map->symbols[map->index[i] - 1], sym);\n\n        if (cmp == 0)\n        {\n            *exact = true;\n            return i;\n        }\n        else if (cmp > 0)\n        {\n            if (i == 0)\n            {\n                break;  // Avoid underflow\n            }\n\n            upper = i - 1;\n        }\n        else\n        {\n            lower = ++i;\n        }\n    }\n\n    assert(!*exact || strcmp(map->symbols[map->index[i] - 1], sym) > 0);\n    return i;\n}\n\nuint32_t symap_try_map(Symap *map, const char *sym)\n{\n    bool exact;\n    const uint32_t index = symap_search(map, sym, &exact);\n\n    if (exact)\n    {\n        assert(!strcmp(map->symbols[map->index[index]], sym));\n        return map->index[index];\n    }\n\n    return 0;\n}\n\nuint32_t symap_map(Symap *map, const char *sym)\n{\n    bool exact;\n    const uint32_t index = symap_search(map, sym, &exact);\n\n    if (exact)\n    {\n        assert(!strcmp(map->symbols[map->index[index] - 1], sym));\n        return map->index[index];\n    }\n\n    const uint32_t id = ++map->size;\n    char *const str = symap_strdup(sym);\n\n    /* Append new symbol to symbols array */\n    map->symbols = (char **) realloc(map->symbols, map->size * sizeof(str));\n    map->symbols[id - 1] = str;\n\n    /* Insert new index element into sorted index */\n    map->index = (uint32_t *) realloc(map->index, map->size * sizeof(uint32_t));\n\n    if (index < map->size - 1)\n    {\n        memmove(map->index + index + 1,\n                map->index + index,\n                (map->size - index - 1) * sizeof(uint32_t));\n    }\n\n    map->index[index] = id;\n\n    return id;\n}\n\nconst char* symap_unmap(Symap *map, uint32_t id)\n{\n    if (id == 0)\n    {\n        return nullptr;\n    }\n    else if (id <= map->size)\n    {\n        return map->symbols[id - 1];\n    }\n\n    return nullptr;\n}\n\n}\n"
  },
  {
    "path": "third-party/optionparser/Makefile",
    "content": "CXX=g++\nCXXFLAGS=-W -Wall -g -fmessage-length=0\nCCFLAGS=$(CXXFLAGS) -fno-exceptions -fno-rtti -nodefaultlibs\nOPTIMIZE=-Os -fomit-frame-pointer\n#OPTIMIZE=-O0\nLD=gcc  # gcc automatically picks the correct crt*.o and helper libraries to link\nVERSION=1.4\nPACKAGEDIR=optionparser-$(VERSION)\nTARBALL=../$(PACKAGEDIR).tar.gz\n\n# all: example_arg testparse testprintusage testodr example doc\nall: example_arg example\n\n# .cpp files depend on the C++ standard lib\n%: %.cpp optionparser.h\n\t$(CXX) $(CXXFLAGS) $(OPTIMIZE) -o $@ $<\n\n# .cc files depend only on libc \n%.o: %.cc optionparser.h\n\t$(CXX) $(CCFLAGS) $(OPTIMIZE) -c $<\n\n%: %.cc optionparser.h\n\t$(CXX) $(CCFLAGS) $(OPTIMIZE) -lc -o $@ $<\n\ntestprintusage: testprintusage.cpp optionparser.h\n\t$(CXX) $(CXXFLAGS) $(OPTIMIZE) -Wno-unused-result -o $@ $<\n\n# testodr needs to be linked in a separate step rather than\n# just passing both .cpp files to g++, because only this way\n# can we be sure that duplicate definitions cause an error message.\ntestodr: testodr1.o testodr2.o\n\t$(LD) -lc -o $@ $^\n\ndoc:\n\tcd .. && doxygen\n\tcd .. && cat Doxyfile | sed '/^INTERNAL_DOCS/s/YES/NO/;/^HTML_OUTPUT/s/-dev//' | doxygen -\n\nclean:\n\trm -f testodr testodr1.o testodr2.o example.o example testprintusage testparse example_arg\n\trm -rf ../html ../html-dev $(PACKAGEDIR) $(TARBALL)\n\trm -f *~ ../*~\n\npackage:\n\trm -rf $(PACKAGEDIR)\n\tmkdir -p $(PACKAGEDIR)/src\n\tcp ../Doxyfile ../DoxygenLayout.xml ../README_ARGC_ARGV $(PACKAGEDIR)\n\tcp Makefile example.cpp example_arg.cc optionparser.h \\\n\t   testodr1.cc testodr2.cc testparse.cpp testprintusage.cpp \\\n\t   printUsage.h $(PACKAGEDIR)/src\n\ttar --owner=0 --group=0 -czf $(TARBALL) $(PACKAGEDIR)\n\nupload: all package\n\tcd .. && scp -r src/optionparser.h html/* optionparser-frs:/home/project-web/optionparser/htdocs\n\tscp $(TARBALL) optionparser-frs:/home/frs/project/o/op/optionparser\n"
  },
  {
    "path": "third-party/optionparser/example.cpp",
    "content": "/* Written 2012 by Matthias S. Benkmann\n *\n * The author hereby waives all copyright and related rights to the contents\n * of this example file (example.cpp) to the extent possible under the law.\n */\n\n/**\n * @file\n * @brief Small demo of The Lean Mean C++ Option Parser.\n *\n * @include example.cpp\n */\n\n#include <iostream>\n#include <string>\n#include <vector>\n#include \"optionparser.h\"\n\nenum  optionIndex { UNKNOWN, HELP, PLUS };\nconst option::Descriptor usage[] =\n{\n {UNKNOWN, 0, \"\", \"\",option::Arg::None, \"USAGE: example [options]\\n\\n\"\n                                        \"Options:\" },\n {HELP, 0,\"\", \"help\",option::Arg::None, \"  --help  \\tPrint usage and exit.\" },\n {PLUS, 0,\"p\",\"plus\",option::Arg::None, \"  --plus, -p  \\tIncrement count.\" },\n {UNKNOWN, 0, \"\", \"\",option::Arg::None, \"\\nExamples:\\n\"\n                               \"  example --unknown -- --this_is_no_option\\n\"\n                               \"  example -unk --plus -ppp file1 file2\\n\" },\n {0,0,0,0,0,0}\n};\n\nint main(int argc, char* argv[])\n{\n  argc-=(argc>0); argv+=(argc>0); // skip program name argv[0] if present\n  option::Stats  stats(usage, argc, argv);\n  std::vector<option::Option> options(stats.options_max);\n  std::vector<option::Option> buffer(stats.buffer_max);\n  option::Parser parse(usage, argc, argv, &options[0], &buffer[0]);\n\n  if (parse.error())\n    return 1;\n\n  if (options[HELP] || argc == 0) {\n    option::printUsage(std::cout, usage);\n    return 0;\n  }\n\n  std::cout << \"--plus count: \" <<\n      options[PLUS].count() << \"\\n\";\n\n  for (option::Option* opt = options[UNKNOWN]; opt; opt = opt->next())\n    std::cout << \"Unknown option: \" << std::string(opt->name,opt->namelen) << \"\\n\";\n\n  for (int i = 0; i < parse.nonOptionsCount(); ++i)\n    std::cout << \"Non-option #\" << i << \": \" << parse.nonOption(i) << \"\\n\";\n}\n"
  },
  {
    "path": "third-party/optionparser/example_arg.cc",
    "content": "/* Written 2012 by Matthias S. Benkmann\n *\n * The author hereby waives all copyright and related rights to the contents\n * of this example file (example_arg.cc) to the extent possible under the law.\n */\n\n/**\n * @file\n * @brief Demonstrates handling various types of option arguments (required, numeric,...) with\n   no dependency on the C++ standard library (only C lib).\n *\n * @include example_arg.cc\n */\n\n#include <stdio.h>\n#include <stdlib.h>\n#include \"optionparser.h\"\n\nstruct Arg: public option::Arg\n{\n  static void printError(const char* msg1, const option::Option& opt, const char* msg2)\n  {\n    fprintf(stderr, \"%s\", msg1);\n    fwrite(opt.name, opt.namelen, 1, stderr);\n    fprintf(stderr, \"%s\", msg2);\n  }\n\n  static option::ArgStatus Unknown(const option::Option& option, bool msg)\n  {\n    if (msg) printError(\"Unknown option '\", option, \"'\\n\");\n    return option::ARG_ILLEGAL;\n  }\n\n  static option::ArgStatus Required(const option::Option& option, bool msg)\n  {\n    if (option.arg != 0)\n      return option::ARG_OK;\n\n    if (msg) printError(\"Option '\", option, \"' requires an argument\\n\");\n    return option::ARG_ILLEGAL;\n  }\n\n  static option::ArgStatus NonEmpty(const option::Option& option, bool msg)\n  {\n    if (option.arg != 0 && option.arg[0] != 0)\n      return option::ARG_OK;\n\n    if (msg) printError(\"Option '\", option, \"' requires a non-empty argument\\n\");\n    return option::ARG_ILLEGAL;\n  }\n\n  static option::ArgStatus Numeric(const option::Option& option, bool msg)\n  {\n    char* endptr = 0;\n    if (option.arg != 0 && strtol(option.arg, &endptr, 10)){};\n    if (endptr != option.arg && *endptr == 0)\n      return option::ARG_OK;\n\n    if (msg) printError(\"Option '\", option, \"' requires a numeric argument\\n\");\n    return option::ARG_ILLEGAL;\n  }\n};\n\nenum  optionIndex { UNKNOWN, HELP, OPTIONAL, REQUIRED, NUMERIC, NONEMPTY };\nconst option::Descriptor usage[] = {\n{ UNKNOWN, 0,\"\", \"\",        Arg::Unknown, \"USAGE: example_arg [options]\\n\\n\"\n                                          \"Options:\" },\n{ HELP,    0,\"\", \"help\",    Arg::None,    \"  \\t--help  \\tPrint usage and exit.\" },\n{ OPTIONAL,0,\"o\",\"optional\",Arg::Optional,\"  -o[<arg>], \\t--optional[=<arg>]\"\n                                          \"  \\tTakes an argument but is happy without one.\" },\n{ REQUIRED,0,\"r\",\"required\",Arg::Required,\"  -r <arg>, \\t--required=<arg>  \\tMust have an argument.\" },\n{ NUMERIC, 0,\"n\",\"numeric\", Arg::Numeric, \"  -n <num>, \\t--numeric=<num>  \\tRequires a number as argument.\" },\n{ NONEMPTY,0,\"1\",\"nonempty\",Arg::NonEmpty,\"  -1 <arg>, \\t--nonempty=<arg>\"\n                                          \"  \\tCan NOT take the empty string as argument.\" },\n{ UNKNOWN, 0,\"\", \"\",        Arg::None,\n \"\\nExamples:\\n\"\n \"  example_arg --unknown -o -n10 \\n\"\n \"  example_arg -o -n10 file1 file2 \\n\"\n \"  example_arg -nfoo file1 file2 \\n\"\n \"  example_arg --optional -- file1 file2 \\n\"\n \"  example_arg --optional file1 file2 \\n\"\n \"  example_arg --optional=file1 file2 \\n\"\n \"  example_arg --optional=  file1 file2 \\n\"\n \"  example_arg -o file1 file2 \\n\"\n \"  example_arg -ofile1 file2 \\n\"\n \"  example_arg -unk file1 file2 \\n\"\n \"  example_arg -r -- file1 \\n\"\n \"  example_arg -r file1 \\n\"\n \"  example_arg --required \\n\"\n \"  example_arg --required=file1 \\n\"\n \"  example_arg --nonempty= file1 \\n\"\n \"  example_arg --nonempty=foo --numeric=999 --optional=bla file1 \\n\"\n \"  example_arg -1foo \\n\"\n \"  example_arg -1 -- \\n\"\n \"  example_arg -1 \\\"\\\" \\n\"\n},\n{ 0, 0, 0, 0, 0, 0 } };\n\nint main(int argc, char* argv[])\n{\n  argc-=(argc>0); argv+=(argc>0); // skip program name argv[0] if present\n  option::Stats stats(usage, argc, argv);\n\n// #ifdef __GNUC__\n//     // GCC supports C99 VLAs for C++ with proper constructor calls.\n//   option::Option options[stats.options_max], buffer[stats.buffer_max];\n// #else\n    // use calloc() to allocate 0-initialized memory. It's not the same\n    // as properly constructed elements, but good enough. Obviously in an\n    // ordinary C++ program you'd use new[], but this file demonstrates that\n    // TLMC++OP can be used without any dependency on the C++ standard library.\n  option::Option* options = (option::Option*)calloc(stats.options_max, sizeof(option::Option));\n  option::Option* buffer  = (option::Option*)calloc(stats.buffer_max,  sizeof(option::Option));\n// #endif\n\n  option::Parser parse(usage, argc, argv, options, buffer);\n\n  if (parse.error())\n    return 1;\n\n  if (options[HELP] || argc == 0)\n  {\n    int columns = getenv(\"COLUMNS\")? atoi(getenv(\"COLUMNS\")) : 80;\n    option::printUsage(fwrite, stdout, usage, columns);\n    return 0;\n  }\n\n  for (int i = 0; i < parse.optionsCount(); ++i)\n  {\n    option::Option& opt = buffer[i];\n    fprintf(stdout, \"Argument #%d is \", i);\n    switch (opt.index())\n    {\n      case HELP:\n        // not possible, because handled further above and exits the program\n      case OPTIONAL:\n        if (opt.arg)\n          fprintf(stdout, \"--optional with optional argument '%s'\\n\", opt.arg);\n        else\n          fprintf(stdout, \"--optional without the optional argument\\n\");\n        break;\n      case REQUIRED:\n        fprintf(stdout, \"--required with argument '%s'\\n\", opt.arg);\n        break;\n      case NUMERIC:\n        fprintf(stdout, \"--numeric with argument '%s'\\n\", opt.arg);\n        break;\n      case NONEMPTY:\n        fprintf(stdout, \"--nonempty with argument '%s'\\n\", opt.arg);\n        break;\n      case UNKNOWN:\n        // not possible because Arg::Unknown returns ARG_ILLEGAL\n        // which aborts the parse with an error\n        break;\n    }\n  }\n\n  for (int i = 0; i < parse.nonOptionsCount(); ++i)\n    fprintf(stdout, \"Non-option argument #%d is %s\\n\", i, parse.nonOption(i));\n}\n"
  },
  {
    "path": "third-party/optionparser/optionparser.h",
    "content": "/*\n * The Lean Mean C++ Option Parser\n *\n * Copyright (C) 2012 Matthias S. Benkmann\n *\n * The \"Software\" in the following 2 paragraphs refers to this file containing\n * the code to The Lean Mean C++ Option Parser.\n * The \"Software\" does NOT refer to any other files which you\n * may have received alongside this file (e.g. as part of a larger project that\n * incorporates The Lean Mean C++ Option Parser).\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software, to deal in the Software without restriction, including\n * without limitation the rights to use, copy, modify, merge, publish,\n * distribute, sublicense, and/or sell copies of the Software, and to permit\n * persons to whom the Software is furnished to do so, subject to the following\n * conditions:\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/*\n * NOTE: It is recommended that you read the processed HTML doxygen documentation\n * rather than this source. If you don't know doxygen, it's like javadoc for C++.\n * If you don't want to install doxygen you can find a copy of the processed\n * documentation at\n *\n * http://optionparser.sourceforge.net/\n *\n */\n\n/**\n * @file\n *\n * @brief This is the only file required to use The Lean Mean C++ Option Parser.\n *        Just \\#include it and you're set.\n *\n * The Lean Mean C++ Option Parser handles the program's command line arguments \n * (argc, argv).\n * It supports the short and long option formats of getopt(), getopt_long() \n * and getopt_long_only() but has a more convenient interface.\n * The following features set it apart from other option parsers:\n *\n * @par Highlights:\n * <ul style=\"padding-left:1em;margin-left:0\">\n * <li> It is a header-only library. Just <code>\\#include \"optionparser.h\"</code> and you're set.\n * <li> It is freestanding. There are no dependencies whatsoever, not even the\n *      C or C++ standard library.\n * <li> It has a usage message formatter that supports column alignment and\n *      line wrapping. This aids localization because it adapts to\n *      translated strings that are shorter or longer (even if they contain\n *      Asian wide characters).\n * <li> Unlike getopt() and derivatives it doesn't force you to loop through\n *     options sequentially. Instead you can access options directly like this:\n *     <ul style=\"margin-top:.5em\">\n *     <li> Test for presence of a switch in the argument vector:\n *      @code if ( options[QUIET] ) ... @endcode\n *     <li> Evaluate --enable-foo/--disable-foo pair where the last one used wins:\n *     @code if ( options[FOO].last()->type() == DISABLE ) ... @endcode\n *     <li> Cumulative option (-v verbose, -vv more verbose, -vvv even more verbose):\n *     @code int verbosity = options[VERBOSE].count(); @endcode\n *     <li> Iterate over all --file=&lt;fname> arguments:\n *     @code for (Option* opt = options[FILE]; opt; opt = opt->next())\n *   fname = opt->arg; ... @endcode\n *     <li> If you really want to, you can still process all arguments in order:\n *     @code\n *   for (int i = 0; i < p.optionsCount(); ++i) {\n *     Option& opt = buffer[i];\n *     switch(opt.index()) {\n *       case HELP:    ...\n *       case VERBOSE: ...\n *       case FILE:    fname = opt.arg; ...\n *       case UNKNOWN: ...\n *     @endcode\n *     </ul>\n * </ul> @n\n * Despite these features the code size remains tiny. \n * It is smaller than <a href=\"http://uclibc.org\">uClibc</a>'s GNU getopt() and just a\n * couple 100 bytes larger than uClibc's SUSv3 getopt(). @n\n * (This does not include the usage formatter, of course. But you don't have to use that.)\n *\n * @par Download:\n * Tarball with examples and test programs:\n * <a style=\"font-size:larger;font-weight:bold\" href=\"http://sourceforge.net/projects/optionparser/files/optionparser-1.4.tar.gz/download\">optionparser-1.4.tar.gz</a> @n\n * Just the header (this is all you really need):\n * <a style=\"font-size:larger;font-weight:bold\" href=\"http://optionparser.sourceforge.net/optionparser.h\">optionparser.h</a>\n *\n * @par Changelog:\n * <b>Version 1.4:</b> Fixed 2 printUsage() bugs that messed up output with small COLUMNS values @n\n * <b>Version 1.3:</b> Compatible with Microsoft Visual C++. @n\n * <b>Version 1.2:</b> Added @ref option::Option::namelen \"Option::namelen\" and removed the extraction\n *                     of short option characters into a special buffer. @n\n *                     Changed @ref option::Arg::Optional \"Arg::Optional\" to accept arguments if they are attached\n *                     rather than separate. This is what GNU getopt() does and how POSIX recommends\n *                     utilities should interpret their arguments.@n\n * <b>Version 1.1:</b> Optional mode with argument reordering as done by GNU getopt(), so that\n *                     options and non-options can be mixed. See\n *                     @ref option::Parser::parse() \"Parser::parse()\".\n *\n * @par Feedback:\n * Send questions, bug reports, feature requests etc. to: <tt><b>optionparser-feedback<span id=\"antispam\">&nbsp;(a)&nbsp;</span>lists.sourceforge.net</b></tt>\n * @htmlonly <script type=\"text/javascript\">document.getElementById(\"antispam\").innerHTML=\"@\"</script> @endhtmlonly\n *\n *\n * @par Example program:\n * (Note: @c option::* identifiers are links that take you to their documentation.)\n * @code\n * #error EXAMPLE SHORTENED FOR READABILITY. BETTER EXAMPLES ARE IN THE .TAR.GZ!\n * #include <iostream>\n * #include \"optionparser.h\"\n *\n * enum  optionIndex { UNKNOWN, HELP, PLUS };\n * const option::Descriptor usage[] =\n * {\n *  {UNKNOWN, 0,\"\" , \"\"    ,option::Arg::None, \"USAGE: example [options]\\n\\n\"\n *                                             \"Options:\" },\n *  {HELP,    0,\"\" , \"help\",option::Arg::None, \"  --help  \\tPrint usage and exit.\" },\n *  {PLUS,    0,\"p\", \"plus\",option::Arg::None, \"  --plus, -p  \\tIncrement count.\" },\n *  {UNKNOWN, 0,\"\" ,  \"\"   ,option::Arg::None, \"\\nExamples:\\n\"\n *                                             \"  example --unknown -- --this_is_no_option\\n\"\n *                                             \"  example -unk --plus -ppp file1 file2\\n\" },\n *  {0,0,0,0,0,0}\n * };\n *\n * int main(int argc, char* argv[])\n * {\n *   argc-=(argc>0); argv+=(argc>0); // skip program name argv[0] if present\n *   option::Stats  stats(usage, argc, argv);\n *   option::Option options[stats.options_max], buffer[stats.buffer_max];\n *   option::Parser parse(usage, argc, argv, options, buffer);\n *\n *   if (parse.error())\n *     return 1;\n *\n *   if (options[HELP] || argc == 0) {\n *     option::printUsage(std::cout, usage);\n *     return 0;\n *   }\n *\n *   std::cout << \"--plus count: \" <<\n *     options[PLUS].count() << \"\\n\";\n *\n *   for (option::Option* opt = options[UNKNOWN]; opt; opt = opt->next())\n *     std::cout << \"Unknown option: \" << opt->name << \"\\n\";\n *\n *   for (int i = 0; i < parse.nonOptionsCount(); ++i)\n *     std::cout << \"Non-option #\" << i << \": \" << parse.nonOption(i) << \"\\n\";\n * }\n * @endcode\n *\n * @par Option syntax:\n * @li The Lean Mean C++ Option Parser follows POSIX <code>getopt()</code> conventions and supports\n *     GNU-style <code>getopt_long()</code> long options as well as Perl-style single-minus\n *     long options (<code>getopt_long_only()</code>).\n * @li short options have the format @c -X where @c X is any character that fits in a char.\n * @li short options can be grouped, i.e. <code>-X -Y</code> is equivalent to @c -XY.\n * @li a short option may take an argument either separate (<code>-X foo</code>) or\n *     attached (@c -Xfoo). You can make the parser accept the additional format @c -X=foo by\n *     registering @c X as a long option (in addition to being a short option) and\n *     enabling single-minus long options.\n * @li an argument-taking short option may be grouped if it is the last in the group, e.g.\n *     @c -ABCXfoo or <code> -ABCX foo </code> (@c foo is the argument to the @c -X option).\n * @li a lone minus character @c '-' is not treated as an option. It is customarily used where\n *     a file name is expected to refer to stdin or stdout.\n * @li long options have the format @c --option-name.\n * @li the option-name of a long option can be anything and include any characters.\n *     Even @c = characters will work, but don't do that.\n * @li [optional] long options may be abbreviated as long as the abbreviation is unambiguous.\n *     You can set a minimum length for abbreviations.\n * @li [optional] long options may begin with a single minus. The double minus form is always\n *     accepted, too.\n * @li a long option may take an argument either separate (<code> --option arg </code>) or\n *     attached (<code> --option=arg </code>). In the attached form the equals sign is mandatory.\n * @li an empty string can be passed as an attached long option argument: <code> --option-name= </code>.\n *     Note the distinction between an empty string as argument and no argument at all.\n * @li an empty string is permitted as separate argument to both long and short options.\n * @li Arguments to both short and long options may start with a @c '-' character. E.g.\n *     <code> -X-X </code>, <code>-X -X</code> or <code> --long-X=-X </code>. If @c -X\n *     and @c --long-X take an argument, that argument will be @c \"-X\" in all 3 cases.\n * @li If using the built-in @ref option::Arg::Optional \"Arg::Optional\", optional arguments must\n *     be attached.\n * @li the special option @c -- (i.e. without a name) terminates the list of\n *     options. Everything that follows is a non-option argument, even if it starts with\n *     a @c '-' character. The @c -- itself will not appear in the parse results.\n * @li the first argument that doesn't start with @c '-' or @c '--' and does not belong to\n *     a preceding argument-taking option, will terminate the option list and is the\n *     first non-option argument. All following command line arguments are treated as\n *     non-option arguments, even if they start with @c '-' . @n\n *     NOTE: This behaviour is mandated by POSIX, but GNU getopt() only honours this if it is\n *     explicitly requested (e.g. by setting POSIXLY_CORRECT). @n\n *     You can enable the GNU behaviour by passing @c true as first argument to\n *     e.g. @ref option::Parser::parse() \"Parser::parse()\".\n * @li Arguments that look like options (i.e. @c '-' followed by at least 1 character) but\n *     aren't, are NOT treated as non-option arguments. They are treated as unknown options and\n *     are collected into a list of unknown options for error reporting. @n\n *     This means that in order to pass a first non-option\n *     argument beginning with the minus character it is required to use the\n *     @c -- special option, e.g.\n *     @code\n *     program -x -- --strange-filename\n *     @endcode\n *     In this example, @c --strange-filename is a non-option argument. If the @c --\n *     were omitted, it would be treated as an unknown option. @n\n *     See @ref option::Descriptor::longopt for information on how to collect unknown options.\n *\n */\n\n#ifndef OPTIONPARSER_H_\n#define OPTIONPARSER_H_\n\n#ifdef _MSC_VER\n#include <intrin.h>\n#pragma intrinsic(_BitScanReverse)\n#endif\n\n/** @brief The namespace of The Lean Mean C++ Option Parser. */\nnamespace optionparser\n{\n\n#ifdef _MSC_VER\nstruct MSC_Builtin_CLZ\n{\n  static int builtin_clz(unsigned x)\n  {\n    unsigned long index;\n    _BitScanReverse(&index, x);\n    return 32-index; // int is always 32bit on Windows, even for target x64\n  }\n};\n#define __builtin_clz(x) MSC_Builtin_CLZ::builtin_clz(x)\n#endif\n\nclass Option;\n\n/**\n * @brief Possible results when checking if an argument is valid for a certain option.\n *\n * In the case that no argument is provided for an option that takes an\n * optional argument, return codes @c ARG_OK and @c ARG_IGNORE are equivalent.\n */\nenum ArgStatus\n{\n  //! The option does not take an argument.\n  ARG_NONE,\n  //! The argument is acceptable for the option.\n  ARG_OK,\n  //! The argument is not acceptable but that's non-fatal because the option's argument is optional.\n  ARG_IGNORE,\n  //! The argument is not acceptable and that's fatal.\n  ARG_ILLEGAL\n};\n\n/**\n * @brief Signature of functions that check if an argument is valid for a certain type of option.\n *\n * Every Option has such a function assigned in its Descriptor.\n * @code\n * Descriptor usage[] = { {UNKNOWN, 0, \"\", \"\", Arg::None, \"\"}, ... };\n * @endcode\n *\n * A CheckArg function has the following signature:\n * @code ArgStatus CheckArg(const Option& option, bool msg); @endcode\n *\n * It is used to check if a potential argument would be acceptable for the option.\n * It will even be called if there is no argument. In that case @c option.arg will be @c NULL.\n *\n * If @c msg is @c true and the function determines that an argument is not acceptable and\n * that this is a fatal error, it should output a message to the user before\n * returning @ref ARG_ILLEGAL. If @c msg is @c false the function should remain silent (or you\n * will get duplicate messages).\n *\n * See @ref ArgStatus for the meaning of the return values.\n *\n * While you can provide your own functions,\n * often the following pre-defined checks (which never return @ref ARG_ILLEGAL) will suffice:\n *\n * @li @c Arg::None @copybrief Arg::None\n * @li @c Arg::Optional @copybrief Arg::Optional\n *\n */\ntypedef ArgStatus (*CheckArg)(const Option& option, bool msg);\n\n/**\n * @brief Describes an option, its help text (usage) and how it should be parsed.\n *\n * The main input when constructing an option::Parser is an array of Descriptors.\n\n * @par Example:\n * @code\n * enum OptionIndex {CREATE, ...};\n * enum OptionType {DISABLE, ENABLE, OTHER};\n *\n * const option::Descriptor usage[] = {\n *   { CREATE,                                            // index\n *     OTHER,                                             // type\n *     \"c\",                                               // shortopt\n *     \"create\",                                          // longopt\n *     Arg::None,                                         // check_arg\n *     \"--create  Tells the program to create something.\" // help\n *   }\n *   , ...\n * };\n * @endcode\n */\nstruct Descriptor\n{\n  /**\n   * @brief Index of this option's linked list in the array filled in by the parser.\n   *\n   * Command line options whose Descriptors have the same index will end up in the same\n   * linked list in the order in which they appear on the command line. If you have\n   * multiple long option aliases that refer to the same option, give their descriptors\n   * the same @c index.\n   *\n   * If you have options that mean exactly opposite things\n   * (e.g. @c --enable-foo and @c --disable-foo ), you should also give them the same\n   * @c index, but distinguish them through different values for @ref type.\n   * That way they end up in the same list and you can just take the last element of the\n   * list and use its type. This way you get the usual behaviour where switches later\n   * on the command line override earlier ones without having to code it manually.\n   *\n   * @par Tip:\n   * Use an enum rather than plain ints for better readability, as shown in the example\n   * at Descriptor.\n   */\n  const unsigned index;\n\n  /**\n   * @brief Used to distinguish between options with the same @ref index.\n   * See @ref index for details.\n   *\n   * It is recommended that you use an enum rather than a plain int to make your\n   * code more readable.\n   */\n  const int type;\n\n  /**\n   * @brief Each char in this string will be accepted as a short option character.\n   *\n   * The string must not include the minus character @c '-' or you'll get undefined\n   * behaviour.\n   *\n   * If this Descriptor should not have short option characters, use the empty\n   * string \"\". NULL is not permitted here!\n   *\n   * See @ref longopt for more information.\n   */\n  const char* const shortopt;\n\n  /**\n   * @brief The long option name (without the leading @c -- ).\n   *\n   * If this Descriptor should not have a long option name, use the empty\n   * string \"\". NULL is not permitted here!\n   *\n   * While @ref shortopt allows multiple short option characters, each\n   * Descriptor can have only a single long option name. If you have multiple\n   * long option names referring to the same option use separate Descriptors\n   * that have the same @ref index and @ref type. You may repeat\n   * short option characters in such an alias Descriptor but there's no need to.\n   *\n   * @par Dummy Descriptors:\n   * You can use dummy Descriptors with an\n   * empty string for both @ref shortopt and @ref longopt to add text to\n   * the usage that is not related to a specific option. See @ref help.\n   * The first dummy Descriptor will be used for unknown options (see below).\n   *\n   * @par Unknown Option Descriptor:\n   * The first dummy Descriptor in the list of Descriptors,\n   * whose @ref shortopt and @ref longopt are both the empty string, will be used\n   * as the Descriptor for unknown options. An unknown option is a string in\n   * the argument vector that is not a lone minus @c '-' but starts with a minus\n   * character and does not match any Descriptor's @ref shortopt or @ref longopt. @n\n   * Note that the dummy descriptor's @ref check_arg function @e will be called and\n   * its return value will be evaluated as usual. I.e. if it returns @ref ARG_ILLEGAL\n   * the parsing will be aborted with <code>Parser::error()==true</code>. @n\n   * if @c check_arg does not return @ref ARG_ILLEGAL the descriptor's\n   * @ref index @e will be used to pick the linked list into which\n   * to put the unknown option. @n\n   * If there is no dummy descriptor, unknown options will be dropped silently.\n   *\n   */\n  const char* const longopt;\n\n  /**\n   * @brief For each option that matches @ref shortopt or @ref longopt this function\n   * will be called to check a potential argument to the option.\n   *\n   * This function will be called even if there is no potential argument. In that case\n   * it will be passed @c NULL as @c arg parameter. Do not confuse this with the empty\n   * string.\n   *\n   * See @ref CheckArg for more information.\n   */\n  const CheckArg check_arg;\n\n  /**\n   * @brief The usage text associated with the options in this Descriptor.\n   *\n   * You can use option::printUsage() to format your usage message based on\n   * the @c help texts. You can use dummy Descriptors where\n   * @ref shortopt and @ref longopt are both the empty string to add text to\n   * the usage that is not related to a specific option.\n   *\n   * See option::printUsage() for special formatting characters you can use in\n   * @c help to get a column layout.\n   *\n   * @attention\n   * Must be UTF-8-encoded. If your compiler supports C++11 you can use the \"u8\"\n   * prefix to make sure string literals are properly encoded.\n   */\n  const char* help;\n};\n\n/**\n * @brief A parsed option from the command line together with its argument if it has one.\n *\n * The Parser chains all parsed options with the same Descriptor::index together\n * to form a linked list. This allows you to easily implement all of the common ways\n * of handling repeated options and enable/disable pairs.\n *\n * @li Test for presence of a switch in the argument vector:\n *      @code if ( options[QUIET] ) ... @endcode\n * @li Evaluate --enable-foo/--disable-foo pair where the last one used wins:\n *     @code if ( options[FOO].last()->type() == DISABLE ) ... @endcode\n * @li Cumulative option (-v verbose, -vv more verbose, -vvv even more verbose):\n *     @code int verbosity = options[VERBOSE].count(); @endcode\n * @li Iterate over all --file=&lt;fname> arguments:\n *     @code for (Option* opt = options[FILE]; opt; opt = opt->next())\n *   fname = opt->arg; ... @endcode\n */\nclass Option\n{\n  Option* next_;\n  Option* prev_;\npublic:\n  /**\n   * @brief Pointer to this Option's Descriptor.\n   *\n   * Remember that the first dummy descriptor (see @ref Descriptor::longopt) is used\n   * for unknown options.\n   *\n   * @attention\n   * @c desc==NULL signals that this Option is unused. This is the default state of\n   * elements in the result array. You don't need to test @c desc explicitly. You\n   * can simply write something like this:\n   * @code\n   * if (options[CREATE])\n   * {\n   *   ...\n   * }\n   * @endcode\n   * This works because of <code> operator const Option*() </code>.\n   */\n  const Descriptor* desc;\n\n  /**\n   * @brief The name of the option as used on the command line.\n   *\n   * The main purpose of this string is to be presented to the user in messages.\n   *\n   * In the case of a long option, this is the actual @c argv pointer, i.e. the first\n   * character is a '-'. In the case of a short option this points to the option\n   * character within the @c argv string.\n   *\n   * Note that in the case of a short option group or an attached option argument, this\n   * string will contain additional characters following the actual name. Use @ref namelen\n   * to filter out the actual option name only.\n   *\n   */\n  const char* name;\n\n  /**\n   * @brief Pointer to this Option's argument (if any).\n   *\n   * NULL if this option has no argument. Do not confuse this with the empty string which\n   * is a valid argument.\n   */\n  const char* arg;\n\n  /**\n   * @brief The length of the option @ref name.\n   *\n   * Because @ref name points into the actual @c argv string, the option name may be\n   * followed by more characters (e.g. other short options in the same short option group).\n   * This value is the number of bytes (not characters!) that are part of the actual name.\n   *\n   * For a short option, this length is always 1. For a long option this length is always\n   * at least 2 if single minus long options are permitted and at least 3 if they are disabled.\n   *\n   * @note\n   * In the pathological case of a minus within a short option group (e.g. @c -xf-z), this\n   * length is incorrect, because this case will be misinterpreted as a long option and the\n   * name will therefore extend to the string's 0-terminator or a following '=\" character\n   * if there is one. This is irrelevant for most uses of @ref name and @c namelen. If you\n   * really need to distinguish the case of a long and a short option, compare @ref name to\n   * the @c argv pointers. A long option's @c name is always identical to one of them,\n   * whereas a short option's is never.\n   */\n  int namelen;\n\n  /**\n   * @brief Returns Descriptor::type of this Option's Descriptor, or 0 if this Option\n   * is invalid (unused).\n   *\n   * Because this method (and last(), too) can be used even on unused Options with desc==0, you can (provided\n   * you arrange your types properly) switch on type() without testing validity first.\n   * @code\n   * enum OptionType { UNUSED=0, DISABLED=0, ENABLED=1 };\n   * enum OptionIndex { FOO };\n   * const Descriptor usage[] = {\n   *   { FOO, ENABLED,  \"\", \"enable-foo\",  Arg::None, 0 },\n   *   { FOO, DISABLED, \"\", \"disable-foo\", Arg::None, 0 },\n   *   { 0, 0, 0, 0, 0, 0 } };\n   * ...\n   * switch(options[FOO].last()->type()) // no validity check required!\n   * {\n   *   case ENABLED: ...\n   *   case DISABLED: ...  // UNUSED==DISABLED !\n   * }\n   * @endcode\n   */\n  int type() const\n  {\n    return desc == 0 ? 0 : desc->type;\n  }\n\n  /**\n   * @brief Returns Descriptor::index of this Option's Descriptor, or -1 if this Option\n   * is invalid (unused).\n   */\n  int index() const\n  {\n    return desc == 0 ? -1 : (int)desc->index;\n  }\n\n  /**\n   * @brief Returns the number of times this Option (or others with the same Descriptor::index)\n   * occurs in the argument vector.\n   *\n   * This corresponds to the number of elements in the linked list this Option is part of.\n   * It doesn't matter on which element you call count(). The return value is always the same.\n   *\n   * Use this to implement cumulative options, such as -v, -vv, -vvv for\n   * different verbosity levels.\n   *\n   * Returns 0 when called for an unused/invalid option.\n   */\n  int count()\n  {\n    int c = (desc == 0 ? 0 : 1);\n    Option* p = first();\n    while (!p->isLast())\n    {\n      ++c;\n      p = p->next_;\n    };\n    return c;\n  }\n\n  /**\n   * @brief Returns true iff this is the first element of the linked list.\n   *\n   * The first element in the linked list is the first option on the command line\n   * that has the respective Descriptor::index value.\n   *\n   * Returns true for an unused/invalid option.\n   */\n  bool isFirst() const\n  {\n    return isTagged(prev_);\n  }\n\n  /**\n   * @brief Returns true iff this is the last element of the linked list.\n   *\n   * The last element in the linked list is the last option on the command line\n   * that has the respective Descriptor::index value.\n   *\n   * Returns true for an unused/invalid option.\n   */\n  bool isLast() const\n  {\n    return isTagged(next_);\n  }\n\n  /**\n   * @brief Returns a pointer to the first element of the linked list.\n   *\n   * Use this when you want the first occurrence of an option on the command line to\n   * take precedence. Note that this is not the way most programs handle options.\n   * You should probably be using last() instead.\n   *\n   * @note\n   * This method may be called on an unused/invalid option and will return a pointer to the\n   * option itself.\n   */\n  Option* first()\n  {\n    Option* p = this;\n    while (!p->isFirst())\n      p = p->prev_;\n    return p;\n  }\n\n  /**\n   * @brief Returns a pointer to the last element of the linked list.\n   *\n   * Use this when you want the last occurrence of an option on the command line to\n   * take precedence. This is the most common way of handling conflicting options.\n   *\n   * @note\n   * This method may be called on an unused/invalid option and will return a pointer to the\n   * option itself.\n   *\n   * @par Tip:\n   * If you have options with opposite meanings (e.g. @c --enable-foo and @c --disable-foo), you\n   * can assign them the same Descriptor::index to get them into the same list. Distinguish them by\n   * Descriptor::type and all you have to do is check <code> last()->type() </code> to get\n   * the state listed last on the command line.\n   */\n  Option* last()\n  {\n    return first()->prevwrap();\n  }\n\n  /**\n   * @brief Returns a pointer to the previous element of the linked list or NULL if\n   * called on first().\n   *\n   * If called on first() this method returns NULL. Otherwise it will return the\n   * option with the same Descriptor::index that precedes this option on the command\n   * line.\n   */\n  Option* prev()\n  {\n    return isFirst() ? 0 : prev_;\n  }\n\n  /**\n   * @brief Returns a pointer to the previous element of the linked list with wrap-around from\n   * first() to last().\n   *\n   * If called on first() this method returns last(). Otherwise it will return the\n   * option with the same Descriptor::index that precedes this option on the command\n   * line.\n   */\n  Option* prevwrap()\n  {\n    return untag(prev_);\n  }\n\n  /**\n   * @brief Returns a pointer to the next element of the linked list or NULL if called\n   * on last().\n   *\n   * If called on last() this method returns NULL. Otherwise it will return the\n   * option with the same Descriptor::index that follows this option on the command\n   * line.\n   */\n  Option* next()\n  {\n    return isLast() ? 0 : next_;\n  }\n\n  /**\n   * @brief Returns a pointer to the next element of the linked list with wrap-around from\n   * last() to first().\n   *\n   * If called on last() this method returns first(). Otherwise it will return the\n   * option with the same Descriptor::index that follows this option on the command\n   * line.\n   */\n  Option* nextwrap()\n  {\n    return untag(next_);\n  }\n\n  /**\n   * @brief Makes @c new_last the new last() by chaining it into the list after last().\n   *\n   * It doesn't matter which element you call append() on. The new element will always\n   * be appended to last().\n   *\n   * @attention\n   * @c new_last must not yet be part of a list, or that list will become corrupted, because\n   * this method does not unchain @c new_last from an existing list.\n   */\n  void append(Option* new_last)\n  {\n    Option* p = last();\n    Option* f = first();\n    p->next_ = new_last;\n    new_last->prev_ = p;\n    new_last->next_ = tag(f);\n    f->prev_ = tag(new_last);\n  }\n\n  /**\n   * @brief Casts from Option to const Option* but only if this Option is valid.\n   *\n   * If this Option is valid (i.e. @c desc!=NULL), returns this.\n   * Otherwise returns NULL. This allows testing an Option directly\n   * in an if-clause to see if it is used:\n   * @code\n   * if (options[CREATE])\n   * {\n   *   ...\n   * }\n   * @endcode\n   * It also allows you to write loops like this:\n   * @code for (Option* opt = options[FILE]; opt; opt = opt->next())\n   *   fname = opt->arg; ... @endcode\n   */\n  operator const Option*() const\n  {\n    return desc ? this : 0;\n  }\n\n  /**\n   * @brief Casts from Option to Option* but only if this Option is valid.\n   *\n   * If this Option is valid (i.e. @c desc!=NULL), returns this.\n   * Otherwise returns NULL. This allows testing an Option directly\n   * in an if-clause to see if it is used:\n   * @code\n   * if (options[CREATE])\n   * {\n   *   ...\n   * }\n   * @endcode\n   * It also allows you to write loops like this:\n   * @code for (Option* opt = options[FILE]; opt; opt = opt->next())\n   *   fname = opt->arg; ... @endcode\n   */\n  operator Option*()\n  {\n    return desc ? this : 0;\n  }\n\n  /**\n   * @brief Creates a new Option that is a one-element linked list and has NULL\n   * @ref desc, @ref name, @ref arg and @ref namelen.\n   */\n  Option() :\n      desc(0), name(0), arg(0), namelen(0)\n  {\n    prev_ = tag(this);\n    next_ = tag(this);\n  }\n\n  /**\n   * @brief Creates a new Option that is a one-element linked list and has the given\n   * values for @ref desc, @ref name and @ref arg.\n   *\n   * If @c name_ points at a character other than '-' it will be assumed to refer to a\n   * short option and @ref namelen will be set to 1. Otherwise the length will extend to\n   * the first '=' character or the string's 0-terminator.\n   */\n  Option(const Descriptor* desc_, const char* name_, const char* arg_)\n  {\n    init(desc_, name_, arg_);\n  }\n\n  /**\n   * @brief Makes @c *this a copy of @c orig except for the linked list pointers.\n   *\n   * After this operation @c *this will be a one-element linked list.\n   */\n  void operator=(const Option& orig)\n  {\n    init(orig.desc, orig.name, orig.arg);\n  }\n\n  /**\n   * @brief Makes @c *this a copy of @c orig except for the linked list pointers.\n   *\n   * After this operation @c *this will be a one-element linked list.\n   */\n  Option(const Option& orig)\n  {\n    init(orig.desc, orig.name, orig.arg);\n  }\n\nprivate:\n  /**\n   * @internal\n   * @brief Sets the fields of this Option to the given values (extracting @c name if necessary).\n   *\n   * If @c name_ points at a character other than '-' it will be assumed to refer to a\n   * short option and @ref namelen will be set to 1. Otherwise the length will extend to\n   * the first '=' character or the string's 0-terminator.\n   */\n  void init(const Descriptor* desc_, const char* name_, const char* arg_)\n  {\n    desc = desc_;\n    name = name_;\n    arg = arg_;\n    prev_ = tag(this);\n    next_ = tag(this);\n    namelen = 0;\n    if (name == 0)\n      return;\n    namelen = 1;\n    if (name[0] != '-')\n      return;\n    while (name[namelen] != 0 && name[namelen] != '=')\n      ++namelen;\n  }\n\n  static Option* tag(Option* ptr)\n  {\n    return (Option*) ((unsigned long long) ptr | 1);\n  }\n\n  static Option* untag(Option* ptr)\n  {\n    return (Option*) ((unsigned long long) ptr & ~1ull);\n  }\n\n  static bool isTagged(Option* ptr)\n  {\n    return ((unsigned long long) ptr & 1);\n  }\n};\n\n/**\n * @brief Functions for checking the validity of option arguments.\n *\n * @copydetails CheckArg\n *\n * The following example code\n * can serve as starting place for writing your own more complex CheckArg functions:\n * @code\n * struct Arg: public option::Arg\n * {\n *   static void printError(const char* msg1, const option::Option& opt, const char* msg2)\n *   {\n *     fprintf(stderr, \"ERROR: %s\", msg1);\n *     fwrite(opt.name, opt.namelen, 1, stderr);\n *     fprintf(stderr, \"%s\", msg2);\n *   }\n *\n *   static option::ArgStatus Unknown(const option::Option& option, bool msg)\n *   {\n *     if (msg) printError(\"Unknown option '\", option, \"'\\n\");\n *     return option::ARG_ILLEGAL;\n *   }\n *\n *   static option::ArgStatus Required(const option::Option& option, bool msg)\n *   {\n *     if (option.arg != 0)\n *       return option::ARG_OK;\n *\n *     if (msg) printError(\"Option '\", option, \"' requires an argument\\n\");\n *     return option::ARG_ILLEGAL;\n *   }\n *\n *   static option::ArgStatus NonEmpty(const option::Option& option, bool msg)\n *   {\n *     if (option.arg != 0 && option.arg[0] != 0)\n *       return option::ARG_OK;\n *\n *     if (msg) printError(\"Option '\", option, \"' requires a non-empty argument\\n\");\n *     return option::ARG_ILLEGAL;\n *   }\n *\n *   static option::ArgStatus Numeric(const option::Option& option, bool msg)\n *   {\n *     char* endptr = 0;\n *     if (option.arg != 0 && strtol(option.arg, &endptr, 10)){};\n *     if (endptr != option.arg && *endptr == 0)\n *       return option::ARG_OK;\n *\n *     if (msg) printError(\"Option '\", option, \"' requires a numeric argument\\n\");\n *     return option::ARG_ILLEGAL;\n *   }\n * };\n * @endcode\n */\nstruct Arg\n{\n  //! @brief For options that don't take an argument: Returns ARG_NONE.\n  static ArgStatus None(const Option&, bool)\n  {\n    return ARG_NONE;\n  }\n\n  //! @brief Returns ARG_OK if the argument is attached and ARG_IGNORE otherwise.\n  static ArgStatus Optional(const Option& option, bool)\n  {\n    if (option.arg && option.name[option.namelen] != 0)\n      return ARG_OK;\n    else\n      return ARG_IGNORE;\n  }\n};\n\n/**\n * @brief Determines the minimum lengths of the buffer and options arrays used for Parser.\n *\n * Because Parser doesn't use dynamic memory its output arrays have to be pre-allocated.\n * If you don't want to use fixed size arrays (which may turn out too small, causing\n * command line arguments to be dropped), you can use Stats to determine the correct sizes.\n * Stats work cumulative. You can first pass in your default options and then the real\n * options and afterwards the counts will reflect the union.\n */\nstruct Stats\n{\n  /**\n   * @brief Number of elements needed for a @c buffer[] array to be used for\n   * @ref Parser::parse() \"parsing\" the same argument vectors that were fed\n   * into this Stats object.\n   *\n   * @note\n   * This number is always 1 greater than the actual number needed, to give\n   * you a sentinel element.\n   */\n  unsigned buffer_max;\n\n  /**\n   * @brief Number of elements needed for an @c options[] array to be used for\n   * @ref Parser::parse() \"parsing\" the same argument vectors that were fed\n   * into this Stats object.\n   *\n   * @note\n   * @li This number is always 1 greater than the actual number needed, to give\n   * you a sentinel element.\n   * @li This number depends only on the @c usage, not the argument vectors, because\n   * the @c options array needs exactly one slot for each possible Descriptor::index.\n   */\n  unsigned options_max;\n\n  /**\n   * @brief Creates a Stats object with counts set to 1 (for the sentinel element).\n   */\n  Stats() :\n      buffer_max(1), options_max(1) // 1 more than necessary as sentinel\n  {\n  }\n\n  /**\n   * @brief Creates a new Stats object and immediately updates it for the\n   * given @c usage and argument vector. You may pass 0 for @c argc and/or @c argv,\n   * if you just want to update @ref options_max.\n   *\n   * @note\n   * The calls to Stats methods must match the later calls to Parser methods.\n   * See Parser::parse() for the meaning of the arguments.\n   */\n  Stats(bool gnu, const Descriptor usage[], int argc, const char** argv, int min_abbr_len = 0, //\n        bool single_minus_longopt = false) :\n      buffer_max(1), options_max(1) // 1 more than necessary as sentinel\n  {\n    add(gnu, usage, argc, argv, min_abbr_len, single_minus_longopt);\n  }\n\n  //! @brief Stats(...) with non-const argv.\n  Stats(bool gnu, const Descriptor usage[], int argc, char** argv, int min_abbr_len = 0, //\n        bool single_minus_longopt = false) :\n      buffer_max(1), options_max(1) // 1 more than necessary as sentinel\n  {\n    add(gnu, usage, argc, (const char**) argv, min_abbr_len, single_minus_longopt);\n  }\n\n  //! @brief POSIX Stats(...) (gnu==false).\n  Stats(const Descriptor usage[], int argc, const char** argv, int min_abbr_len = 0, //\n        bool single_minus_longopt = false) :\n      buffer_max(1), options_max(1) // 1 more than necessary as sentinel\n  {\n    add(false, usage, argc, argv, min_abbr_len, single_minus_longopt);\n  }\n\n  //! @brief POSIX Stats(...) (gnu==false) with non-const argv.\n  Stats(const Descriptor usage[], int argc, char** argv, int min_abbr_len = 0, //\n        bool single_minus_longopt = false) :\n      buffer_max(1), options_max(1) // 1 more than necessary as sentinel\n  {\n    add(false, usage, argc, (const char**) argv, min_abbr_len, single_minus_longopt);\n  }\n\n  /**\n   * @brief Updates this Stats object for the\n   * given @c usage and argument vector. You may pass 0 for @c argc and/or @c argv,\n   * if you just want to update @ref options_max.\n   *\n   * @note\n   * The calls to Stats methods must match the later calls to Parser methods.\n   * See Parser::parse() for the meaning of the arguments.\n   */\n  void add(bool gnu, const Descriptor usage[], int argc, const char** argv, int min_abbr_len = 0, //\n           bool single_minus_longopt = false);\n\n  //! @brief add() with non-const argv.\n  void add(bool gnu, const Descriptor usage[], int argc, char** argv, int min_abbr_len = 0, //\n           bool single_minus_longopt = false)\n  {\n    add(gnu, usage, argc, (const char**) argv, min_abbr_len, single_minus_longopt);\n  }\n\n  //! @brief POSIX add() (gnu==false).\n  void add(const Descriptor usage[], int argc, const char** argv, int min_abbr_len = 0, //\n           bool single_minus_longopt = false)\n  {\n    add(false, usage, argc, argv, min_abbr_len, single_minus_longopt);\n  }\n\n  //! @brief POSIX add() (gnu==false) with non-const argv.\n  void add(const Descriptor usage[], int argc, char** argv, int min_abbr_len = 0, //\n           bool single_minus_longopt = false)\n  {\n    add(false, usage, argc, (const char**) argv, min_abbr_len, single_minus_longopt);\n  }\nprivate:\n  class CountOptionsAction;\n};\n\n/**\n * @brief Checks argument vectors for validity and parses them into data\n * structures that are easier to work with.\n *\n * @par Example:\n * @code\n * int main(int argc, char* argv[])\n * {\n *   argc-=(argc>0); argv+=(argc>0); // skip program name argv[0] if present\n *   option::Stats  stats(usage, argc, argv);\n *   option::Option options[stats.options_max], buffer[stats.buffer_max];\n *   option::Parser parse(usage, argc, argv, options, buffer);\n *\n *   if (parse.error())\n *     return 1;\n *\n *   if (options[HELP])\n *   ...\n * @endcode\n */\nclass Parser\n{\n  int op_count; //!< @internal @brief see optionsCount()\n  int nonop_count; //!< @internal @brief see nonOptionsCount()\n  const char** nonop_args; //!< @internal @brief see nonOptions()\n  bool err; //!< @internal @brief see error()\npublic:\n\n  /**\n   * @brief Creates a new Parser.\n   */\n  Parser() :\n      op_count(0), nonop_count(0), nonop_args(0), err(false)\n  {\n  }\n\n  /**\n   * @brief Creates a new Parser and immediately parses the given argument vector.\n   * @copydetails parse()\n   */\n  Parser(bool gnu, const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[],\n         int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1) :\n      op_count(0), nonop_count(0), nonop_args(0), err(false)\n  {\n    parse(gnu, usage, argc, argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  //! @brief Parser(...) with non-const argv.\n  Parser(bool gnu, const Descriptor usage[], int argc, char** argv, Option options[], Option buffer[],\n         int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1) :\n      op_count(0), nonop_count(0), nonop_args(0), err(false)\n  {\n    parse(gnu, usage, argc, (const char**) argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  //! @brief POSIX Parser(...) (gnu==false).\n  Parser(const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[], int min_abbr_len = 0,\n         bool single_minus_longopt = false, int bufmax = -1) :\n      op_count(0), nonop_count(0), nonop_args(0), err(false)\n  {\n    parse(false, usage, argc, argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  //! @brief POSIX Parser(...) (gnu==false) with non-const argv.\n  Parser(const Descriptor usage[], int argc, char** argv, Option options[], Option buffer[], int min_abbr_len = 0,\n         bool single_minus_longopt = false, int bufmax = -1) :\n      op_count(0), nonop_count(0), nonop_args(0), err(false)\n  {\n    parse(false, usage, argc, (const char**) argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  /**\n   * @brief Parses the given argument vector.\n   *\n   * @param gnu if true, parse() will not stop at the first non-option argument. Instead it will\n   *            reorder arguments so that all non-options are at the end. This is the default behaviour\n   *            of GNU getopt() but is not conforming to POSIX. @n\n   *            Note, that once the argument vector has been reordered, the @c gnu flag will have\n   *            no further effect on this argument vector. So it is enough to pass @c gnu==true when\n   *            creating Stats.\n   * @param usage Array of Descriptor objects that describe the options to support. The last entry\n   *              of this array must have 0 in all fields.\n   * @param argc The number of elements from @c argv that are to be parsed. If you pass -1, the number\n   *             will be determined automatically. In that case the @c argv list must end with a NULL\n   *             pointer.\n   * @param argv The arguments to be parsed. If you pass -1 as @c argc the last pointer in the @c argv\n   *             list must be NULL to mark the end.\n   * @param options Each entry is the first element of a linked list of Options. Each new option\n   *                that is parsed will be appended to the list specified by that Option's\n   *                Descriptor::index. If an entry is not yet used (i.e. the Option is invalid),\n   *                it will be replaced rather than appended to. @n\n   *                The minimum length of this array is the greatest Descriptor::index value that\n   *                occurs in @c usage @e PLUS ONE.\n   * @param buffer Each argument that is successfully parsed (including unknown arguments, if they\n   *        have a Descriptor whose CheckArg does not return @ref ARG_ILLEGAL) will be stored in this\n   *        array. parse() scans the array for the first invalid entry and begins writing at that\n   *        index. You can pass @c bufmax to limit the number of options stored.\n   * @param min_abbr_len Passing a value <code> min_abbr_len > 0 </code> enables abbreviated long\n   *               options. The parser will match a prefix of a long option as if it was\n   *               the full long option (e.g. @c --foob=10 will be interpreted as if it was\n   *               @c --foobar=10 ), as long as the prefix has at least @c min_abbr_len characters\n   *               (not counting the @c -- ) and is unambiguous.\n   *               @n Be careful if combining @c min_abbr_len=1 with @c single_minus_longopt=true\n   *               because the ambiguity check does not consider short options and abbreviated\n   *               single minus long options will take precedence over short options.\n   * @param single_minus_longopt Passing @c true for this option allows long options to begin with\n   *               a single minus. The double minus form will still be recognized. Note that\n   *               single minus long options take precedence over short options and short option\n   *               groups. E.g. @c -file would be interpreted as @c --file and not as\n   *               <code> -f -i -l -e </code> (assuming a long option named @c \"file\" exists).\n   * @param bufmax The greatest index in the @c buffer[] array that parse() will write to is\n   *               @c bufmax-1. If there are more options, they will be processed (in particular\n   *               their CheckArg will be called) but not stored. @n\n   *               If you used Stats::buffer_max to dimension this array, you can pass\n   *               -1 (or not pass @c bufmax at all) which tells parse() that the buffer is\n   *               \"large enough\".\n   * @attention\n   * Remember that @c options and @c buffer store Option @e objects, not pointers. Therefore it\n   * is not possible for the same object to be in both arrays. For those options that are found in\n   * both @c buffer[] and @c options[] the respective objects are independent copies. And only the\n   * objects in @c options[] are properly linked via Option::next() and Option::prev().\n   * You can iterate over @c buffer[] to\n   * process all options in the order they appear in the argument vector, but if you want access to\n   * the other Options with the same Descriptor::index, then you @e must access the linked list via\n   * @c options[]. You can get the linked list in options from a buffer object via something like\n   * @c options[buffer[i].index()].\n   */\n  void parse(bool gnu, const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[],\n             int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1);\n\n  //! @brief parse() with non-const argv.\n  void parse(bool gnu, const Descriptor usage[], int argc, char** argv, Option options[], Option buffer[],\n             int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1)\n  {\n    parse(gnu, usage, argc, (const char**) argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  //! @brief POSIX parse() (gnu==false).\n  void parse(const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[],\n             int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1)\n  {\n    parse(false, usage, argc, argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  //! @brief POSIX parse() (gnu==false) with non-const argv.\n  void parse(const Descriptor usage[], int argc, char** argv, Option options[], Option buffer[], int min_abbr_len = 0,\n             bool single_minus_longopt = false, int bufmax = -1)\n  {\n    parse(false, usage, argc, (const char**) argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  /**\n   * @brief Returns the number of valid Option objects in @c buffer[].\n   *\n   * @note\n   * @li The returned value always reflects the number of Options in the buffer[] array used for\n   * the most recent call to parse().\n   * @li The count (and the buffer[]) includes unknown options if they are collected\n   * (see Descriptor::longopt).\n   */\n  int optionsCount()\n  {\n    return op_count;\n  }\n\n  /**\n   * @brief Returns the number of non-option arguments that remained at the end of the\n   * most recent parse() that actually encountered non-option arguments.\n   *\n   * @note\n   * A parse() that does not encounter non-option arguments will leave this value\n   * as well as nonOptions() undisturbed. This means you can feed the Parser a\n   * default argument vector that contains non-option arguments (e.g. a default filename).\n   * Then you feed it the actual arguments from the user. If the user has supplied at\n   * least one non-option argument, all of the non-option arguments from the default\n   * disappear and are replaced by the user's non-option arguments. However, if the\n   * user does not supply any non-option arguments the defaults will still be in\n   * effect.\n   */\n  int nonOptionsCount()\n  {\n    return nonop_count;\n  }\n\n  /**\n   * @brief Returns a pointer to an array of non-option arguments (only valid\n   * if <code>nonOptionsCount() >0 </code>).\n   *\n   * @note\n   * @li parse() does not copy arguments, so this pointer points into the actual argument\n   * vector as passed to parse().\n   * @li As explained at nonOptionsCount() this pointer is only changed by parse() calls\n   * that actually encounter non-option arguments. A parse() call that encounters only\n   * options, will not change nonOptions().\n   */\n  const char** nonOptions()\n  {\n    return nonop_args;\n  }\n\n  /**\n   * @brief Returns <b><code>nonOptions()[i]</code></b> (@e without checking if i is in range!).\n   */\n  const char* nonOption(int i)\n  {\n    return nonOptions()[i];\n  }\n\n  /**\n   * @brief Returns @c true if an unrecoverable error occurred while parsing options.\n   *\n   * An illegal argument to an option (i.e. CheckArg returns @ref ARG_ILLEGAL) is an\n   * unrecoverable error that aborts the parse. Unknown options are only an error if\n   * their CheckArg function returns @ref ARG_ILLEGAL. Otherwise they are collected.\n   * In that case if you want to exit the program if either an illegal argument\n   * or an unknown option has been passed, use code like this\n   *\n   * @code\n   * if (parser.error() || options[UNKNOWN])\n   *   exit(1);\n   * @endcode\n   *\n   */\n  bool error()\n  {\n    return err;\n  }\n\nprivate:\n  friend struct Stats;\n  class StoreOptionAction;\n  struct Action;\n\n  /**\n   * @internal\n   * @brief This is the core function that does all the parsing.\n   * @retval false iff an unrecoverable error occurred.\n   */\n  static bool workhorse(bool gnu, const Descriptor usage[], int numargs, const char** args, Action& action,\n                        bool single_minus_longopt, bool print_errors, int min_abbr_len);\n\n  /**\n   * @internal\n   * @brief Returns true iff @c st1 is a prefix of @c st2 and\n   * in case @c st2 is longer than @c st1, then\n   * the first additional character is '='.\n   *\n   * @par Examples:\n   * @code\n   * streq(\"foo\", \"foo=bar\") == true\n   * streq(\"foo\", \"foobar\")  == false\n   * streq(\"foo\", \"foo\")     == true\n   * streq(\"foo=bar\", \"foo\") == false\n   * @endcode\n   */\n  static bool streq(const char* st1, const char* st2)\n  {\n    while (*st1 != 0)\n      if (*st1++ != *st2++)\n        return false;\n    return (*st2 == 0 || *st2 == '=');\n  }\n\n  /**\n   * @internal\n   * @brief Like streq() but handles abbreviations.\n   *\n   * Returns true iff @c st1 and @c st2 have a common\n   * prefix with the following properties:\n   * @li (if min > 0) its length is at least @c min characters or the same length as @c st1 (whichever is smaller).\n   * @li (if min <= 0) its length is the same as that of @c st1\n   * @li within @c st2 the character following the common prefix is either '=' or end-of-string.\n   *\n   * Examples:\n   * @code\n   * streqabbr(\"foo\", \"foo=bar\",<anything>) == true\n   * streqabbr(\"foo\", \"fo=bar\" , 2) == true\n   * streqabbr(\"foo\", \"fo\"     , 2) == true\n   * streqabbr(\"foo\", \"fo\"     , 0) == false\n   * streqabbr(\"foo\", \"f=bar\"  , 2) == false\n   * streqabbr(\"foo\", \"f\"      , 2) == false\n   * streqabbr(\"fo\" , \"foo=bar\",<anything>)  == false\n   * streqabbr(\"foo\", \"foobar\" ,<anything>)  == false\n   * streqabbr(\"foo\", \"fobar\"  ,<anything>)  == false\n   * streqabbr(\"foo\", \"foo\"    ,<anything>)  == true\n   * @endcode\n   */\n  static bool streqabbr(const char* st1, const char* st2, long long min)\n  {\n    const char* st1start = st1;\n    while (*st1 != 0 && (*st1 == *st2))\n    {\n      ++st1;\n      ++st2;\n    }\n\n    return (*st1 == 0 || (min > 0 && (st1 - st1start) >= min)) && (*st2 == 0 || *st2 == '=');\n  }\n\n  /**\n   * @internal\n   * @brief Returns true iff character @c ch is contained in the string @c st.\n   *\n   * Returns @c true for @c ch==0 .\n   */\n  static bool instr(char ch, const char* st)\n  {\n    while (*st != 0 && *st != ch)\n      ++st;\n    return *st == ch;\n  }\n\n  /**\n   * @internal\n   * @brief Rotates <code>args[-count],...,args[-1],args[0]</code> to become\n   *        <code>args[0],args[-count],...,args[-1]</code>.\n   */\n  static void shift(const char** args, int count)\n  {\n    for (int i = 0; i > -count; --i)\n    {\n      const char* temp = args[i];\n      args[i] = args[i - 1];\n      args[i - 1] = temp;\n    }\n  }\n};\n\n/**\n * @internal\n * @brief Interface for actions Parser::workhorse() should perform for each Option it\n * parses.\n */\nstruct Parser::Action\n{\n  /**\n   * @brief Called by Parser::workhorse() for each Option that has been successfully\n   * parsed (including unknown\n   * options if they have a Descriptor whose Descriptor::check_arg does not return\n   * @ref ARG_ILLEGAL.\n   *\n   * Returns @c false iff a fatal error has occured and the parse should be aborted.\n   */\n  virtual bool perform(Option&)\n  {\n    return true;\n  }\n\n  /**\n   * @brief Called by Parser::workhorse() after finishing the parse.\n   * @param numargs the number of non-option arguments remaining\n   * @param args pointer to the first remaining non-option argument (if numargs > 0).\n   *\n   * @return\n   * @c false iff a fatal error has occurred.\n   */\n  virtual bool finished(int numargs, const char** args)\n  {\n    (void) numargs;\n    (void) args;\n    return true;\n  }\n};\n\n/**\n * @internal\n * @brief An Action to pass to Parser::workhorse() that will increment a counter for\n * each parsed Option.\n */\nclass Stats::CountOptionsAction: public Parser::Action\n{\n  unsigned* buffer_max;\npublic:\n  /**\n   * Creates a new CountOptionsAction that will increase @c *buffer_max_ for each\n   * parsed Option.\n   */\n  CountOptionsAction(unsigned* buffer_max_) :\n      buffer_max(buffer_max_)\n  {\n  }\n\n  bool perform(Option&)\n  {\n    if (*buffer_max == 0x7fffffff)\n      return false; // overflow protection: don't accept number of options that doesn't fit signed int\n    ++*buffer_max;\n    return true;\n  }\n};\n\n/**\n * @internal\n * @brief An Action to pass to Parser::workhorse() that will store each parsed Option in\n * appropriate arrays (see Parser::parse()).\n */\nclass Parser::StoreOptionAction: public Parser::Action\n{\n  Parser& parser;\n  Option* options;\n  Option* buffer;\n  int bufmax; //! Number of slots in @c buffer. @c -1 means \"large enough\".\npublic:\n  /**\n   * @brief Creates a new StoreOption action.\n   * @param parser_ the parser whose op_count should be updated.\n   * @param options_ each Option @c o is chained into the linked list @c options_[o.desc->index]\n   * @param buffer_ each Option is appended to this array as long as there's a free slot.\n   * @param bufmax_ number of slots in @c buffer_. @c -1 means \"large enough\".\n   */\n  StoreOptionAction(Parser& parser_, Option options_[], Option buffer_[], int bufmax_) :\n      parser(parser_), options(options_), buffer(buffer_), bufmax(bufmax_)\n  {\n    // find first empty slot in buffer (if any)\n    int bufidx = 0;\n    while ((bufmax < 0 || bufidx < bufmax) && buffer[bufidx])\n      ++bufidx;\n\n    // set parser's optionCount\n    parser.op_count = bufidx;\n  }\n\n  bool perform(Option& option)\n  {\n    if (bufmax < 0 || parser.op_count < bufmax)\n    {\n      if (parser.op_count == 0x7fffffff)\n        return false; // overflow protection: don't accept number of options that doesn't fit signed int\n\n      buffer[parser.op_count] = option;\n      int idx = buffer[parser.op_count].desc->index;\n      if (options[idx])\n        options[idx].append(buffer[parser.op_count]);\n      else\n        options[idx] = buffer[parser.op_count];\n      ++parser.op_count;\n    }\n    return true; // NOTE: an option that is discarded because of a full buffer is not fatal\n  }\n\n  bool finished(int numargs, const char** args)\n  {\n    // only overwrite non-option argument list if there's at least 1\n    // new non-option argument. Otherwise we keep the old list. This\n    // makes it easy to use default non-option arguments.\n    if (numargs > 0)\n    {\n      parser.nonop_count = numargs;\n      parser.nonop_args = args;\n    }\n\n    return true;\n  }\n};\n\ninline void Parser::parse(bool gnu, const Descriptor usage[], int argc, const char** argv, Option options[],\n                          Option buffer[], int min_abbr_len, bool single_minus_longopt, int bufmax)\n{\n  StoreOptionAction action(*this, options, buffer, bufmax);\n  err = !workhorse(gnu, usage, argc, argv, action, single_minus_longopt, true, min_abbr_len);\n}\n\ninline void Stats::add(bool gnu, const Descriptor usage[], int argc, const char** argv, int min_abbr_len,\n                       bool single_minus_longopt)\n{\n  // determine size of options array. This is the greatest index used in the usage + 1\n  int i = 0;\n  while (usage[i].shortopt != 0)\n  {\n    if (usage[i].index + 1 >= options_max)\n      options_max = (usage[i].index + 1) + 1; // 1 more than necessary as sentinel\n\n    ++i;\n  }\n\n  CountOptionsAction action(&buffer_max);\n  Parser::workhorse(gnu, usage, argc, argv, action, single_minus_longopt, false, min_abbr_len);\n}\n\ninline bool Parser::workhorse(bool gnu, const Descriptor usage[], int numargs, const char** args, Action& action,\n                              bool single_minus_longopt, bool print_errors, int min_abbr_len)\n{\n  // protect against NULL pointer\n  if (args == 0)\n    numargs = 0;\n\n  int nonops = 0;\n\n  while (numargs != 0 && *args != 0)\n  {\n    const char* param = *args; // param can be --long-option, -srto or non-option argument\n\n    // in POSIX mode the first non-option argument terminates the option list\n    // a lone minus character is a non-option argument\n    if (param[0] != '-' || param[1] == 0)\n    {\n      if (gnu)\n      {\n        ++nonops;\n        ++args;\n        if (numargs > 0)\n          --numargs;\n        continue;\n      }\n      else\n        break;\n    }\n\n    // -- terminates the option list. The -- itself is skipped.\n    if (param[1] == '-' && param[2] == 0)\n    {\n      shift(args, nonops);\n      ++args;\n      if (numargs > 0)\n        --numargs;\n      break;\n    }\n\n    bool handle_short_options;\n    const char* longopt_name;\n    if (param[1] == '-') // if --long-option\n    {\n      handle_short_options = false;\n      longopt_name = param + 2;\n    }\n    else\n    {\n      handle_short_options = true;\n      longopt_name = param + 1; //for testing a potential -long-option\n    }\n\n    bool try_single_minus_longopt = single_minus_longopt;\n    bool have_more_args = (numargs > 1 || numargs < 0); // is referencing argv[1] valid?\n\n    do // loop over short options in group, for long options the body is executed only once\n    {\n      int idx = 0;\n\n      const char* optarg = nullptr;\n\n      /******************** long option **********************/\n      if (handle_short_options == false || try_single_minus_longopt)\n      {\n        idx = 0;\n        while (usage[idx].longopt != 0 && !streq(usage[idx].longopt, longopt_name))\n          ++idx;\n\n        if (usage[idx].longopt == 0 && min_abbr_len > 0) // if we should try to match abbreviated long options\n        {\n          int i1 = 0;\n          while (usage[i1].longopt != 0 && !streqabbr(usage[i1].longopt, longopt_name, min_abbr_len))\n            ++i1;\n          if (usage[i1].longopt != 0)\n          { // now test if the match is unambiguous by checking for another match\n            int i2 = i1 + 1;\n            while (usage[i2].longopt != 0 && !streqabbr(usage[i2].longopt, longopt_name, min_abbr_len))\n              ++i2;\n\n            if (usage[i2].longopt == 0) // if there was no second match it's unambiguous, so accept i1 as idx\n              idx = i1;\n          }\n        }\n\n        // if we found something, disable handle_short_options (only relevant if single_minus_longopt)\n        if (usage[idx].longopt != 0)\n          handle_short_options = false;\n\n        try_single_minus_longopt = false; // prevent looking for longopt in the middle of shortopt group\n\n        optarg = longopt_name;\n        while (*optarg != 0 && *optarg != '=')\n          ++optarg;\n        if (*optarg == '=') // attached argument\n          ++optarg;\n        else\n          // possibly detached argument\n          optarg = (have_more_args ? args[1] : 0);\n      }\n\n      /************************ short option ***********************************/\n      if (handle_short_options)\n      {\n        if (*++param == 0) // point at the 1st/next option character\n          break; // end of short option group\n\n        idx = 0;\n        while (usage[idx].shortopt != 0 && !instr(*param, usage[idx].shortopt))\n          ++idx;\n\n        if (param[1] == 0) // if the potential argument is separate\n          optarg = (have_more_args ? args[1] : 0);\n        else\n          // if the potential argument is attached\n          optarg = param + 1;\n      }\n\n      const Descriptor* descriptor = &usage[idx];\n\n      if (descriptor->shortopt == 0) /**************  unknown option ********************/\n      {\n        // look for dummy entry (shortopt == \"\" and longopt == \"\") to use as Descriptor for unknown options\n        idx = 0;\n        while (usage[idx].shortopt != 0 && (usage[idx].shortopt[0] != 0 || usage[idx].longopt[0] != 0))\n          ++idx;\n        descriptor = (usage[idx].shortopt == 0 ? 0 : &usage[idx]);\n      }\n\n      if (descriptor != 0)\n      {\n        Option option(descriptor, param, optarg);\n        switch (descriptor->check_arg(option, print_errors))\n        {\n          case ARG_ILLEGAL:\n            return false; // fatal\n          case ARG_OK:\n            // skip one element of the argument vector, if it's a separated argument\n            if (optarg != 0 && have_more_args && optarg == args[1])\n            {\n              shift(args, nonops);\n              if (numargs > 0)\n                --numargs;\n              ++args;\n            }\n\n            // No further short options are possible after an argument\n            handle_short_options = false;\n\n            break;\n          case ARG_IGNORE:\n          case ARG_NONE:\n            option.arg = 0;\n            break;\n        }\n\n        if (!action.perform(option))\n          return false;\n      }\n\n    } while (handle_short_options);\n\n    shift(args, nonops);\n    ++args;\n    if (numargs > 0)\n      --numargs;\n\n  } // while\n\n  if (numargs > 0 && *args == 0) // It's a bug in the caller if numargs is greater than the actual number\n    numargs = 0; // of arguments, but as a service to the user we fix this if we spot it.\n\n  if (numargs < 0) // if we don't know the number of remaining non-option arguments\n  { // we need to count them\n    numargs = 0;\n    while (args[numargs] != 0)\n      ++numargs;\n  }\n\n  return action.finished(numargs + nonops, args - nonops);\n}\n\n/**\n * @internal\n * @brief The implementation of option::printUsage().\n */\nstruct PrintUsageImplementation\n{\n  /**\n   * @internal\n   * @brief Interface for Functors that write (part of) a string somewhere.\n   */\n  struct IStringWriter\n  {\n    /**\n     * @brief Writes the given number of chars beginning at the given pointer somewhere.\n     */\n    virtual void operator()(const char*, int)\n    {\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Encapsulates a function with signature <code>func(string, size)</code> where\n   * string can be initialized with a const char* and size with an int.\n   */\n  template<typename Function>\n  struct FunctionWriter: public IStringWriter\n  {\n    Function* write;\n\n    virtual void operator()(const char* str, int size)\n    {\n      (*write)(str, size);\n    }\n\n    FunctionWriter(Function* w) :\n        write(w)\n    {\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Encapsulates a reference to an object with a <code>write(string, size)</code>\n   * method like that of @c std::ostream.\n   */\n  template<typename OStream>\n  struct OStreamWriter: public IStringWriter\n  {\n    OStream& ostream;\n\n    virtual void operator()(const char* str, int size)\n    {\n      ostream.write(str, size);\n    }\n\n    OStreamWriter(OStream& o) :\n        ostream(o)\n    {\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Like OStreamWriter but encapsulates a @c const reference, which is\n   * typically a temporary object of a user class.\n   */\n  template<typename Temporary>\n  struct TemporaryWriter: public IStringWriter\n  {\n    const Temporary& userstream;\n\n    virtual void operator()(const char* str, int size)\n    {\n      userstream.write(str, size);\n    }\n\n    TemporaryWriter(const Temporary& u) :\n        userstream(u)\n    {\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Encapsulates a function with the signature <code>func(fd, string, size)</code> (the\n   * signature of the @c write() system call)\n   * where fd can be initialized from an int, string from a const char* and size from an int.\n   */\n  template<typename Syscall>\n  struct SyscallWriter: public IStringWriter\n  {\n    Syscall* write;\n    int fd;\n\n    virtual void operator()(const char* str, int size)\n    {\n      (*write)(fd, str, size);\n    }\n\n    SyscallWriter(Syscall* w, int f) :\n        write(w), fd(f)\n    {\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Encapsulates a function with the same signature as @c std::fwrite().\n   */\n  template<typename Function, typename Stream>\n  struct StreamWriter: public IStringWriter\n  {\n    Function* fwrite;\n    Stream* stream;\n\n    virtual void operator()(const char* str, int size)\n    {\n      (*fwrite)(str, size, 1, stream);\n    }\n\n    StreamWriter(Function* w, Stream* s) :\n        fwrite(w), stream(s)\n    {\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Sets <code> i1 = max(i1, i2) </code>\n   */\n  static void upmax(int& i1, int i2)\n  {\n    i1 = (i1 >= i2 ? i1 : i2);\n  }\n\n  /**\n   * @internal\n   * @brief Moves the \"cursor\" to column @c want_x assuming it is currently at column @c x\n   * and sets @c x=want_x .\n   * If <code> x > want_x </code>, a line break is output before indenting.\n   *\n   * @param write Spaces and possibly a line break are written via this functor to get\n   *        the desired indentation @c want_x .\n   * @param[in,out] x the current indentation. Set to @c want_x by this method.\n   * @param want_x the desired indentation.\n   */\n  static void indent(IStringWriter& write, int& x, int want_x)\n  {\n    int indent = want_x - x;\n    if (indent < 0)\n    {\n      write(\"\\n\", 1);\n      indent = want_x;\n    }\n\n    if (indent > 0)\n    {\n      char space = ' ';\n      for (int i = 0; i < indent; ++i)\n        write(&space, 1);\n      x = want_x;\n    }\n  }\n\n  /**\n   * @brief Returns true if ch is the unicode code point of a wide character.\n   *\n   * @note\n   * The following character ranges are treated as wide\n   * @code\n   * 1100..115F\n   * 2329..232A  (just 2 characters!)\n   * 2E80..A4C6  except for 303F\n   * A960..A97C\n   * AC00..D7FB\n   * F900..FAFF\n   * FE10..FE6B\n   * FF01..FF60\n   * FFE0..FFE6\n   * 1B000......\n   * @endcode\n   */\n  static bool isWideChar(unsigned ch)\n  {\n    if (ch == 0x303F)\n      return false;\n\n    return ((0x1100 <= ch && ch <= 0x115F) || (0x2329 <= ch && ch <= 0x232A) || (0x2E80 <= ch && ch <= 0xA4C6)\n        || (0xA960 <= ch && ch <= 0xA97C) || (0xAC00 <= ch && ch <= 0xD7FB) || (0xF900 <= ch && ch <= 0xFAFF)\n        || (0xFE10 <= ch && ch <= 0xFE6B) || (0xFF01 <= ch && ch <= 0xFF60) || (0xFFE0 <= ch && ch <= 0xFFE6)\n        || (0x1B000 <= ch));\n  }\n\n  /**\n   * @internal\n   * @brief Splits a @c Descriptor[] array into tables, rows, lines and columns and\n   * iterates over these components.\n   *\n   * The top-level organizational unit is the @e table.\n   * A table begins at a Descriptor with @c help!=NULL and extends up to\n   * a Descriptor with @c help==NULL.\n   *\n   * A table consists of @e rows. Due to line-wrapping and explicit breaks\n   * a row may take multiple lines on screen. Rows within the table are separated\n   * by \\\\n. They never cross Descriptor boundaries. This means a row ends either\n   * at \\\\n or the 0 at the end of the help string.\n   *\n   * A row consists of columns/cells. Columns/cells within a row are separated by \\\\t.\n   * Line breaks within a cell are marked by \\\\v.\n   *\n   * Rows in the same table need not have the same number of columns/cells. The\n   * extreme case are interjections, which are rows that contain neither \\\\t nor \\\\v.\n   * These are NOT treated specially by LinePartIterator, but they are treated\n   * specially by printUsage().\n   *\n   * LinePartIterator iterates through the usage at 3 levels: table, row and part.\n   * Tables and rows are as described above. A @e part is a line within a cell.\n   * LinePartIterator iterates through 1st parts of all cells, then through the 2nd\n   * parts of all cells (if any),... @n\n   * Example: The row <code> \"1 \\v 3 \\t 2 \\v 4\" </code> has 2 cells/columns and 4 parts.\n   * The parts will be returned in the order 1, 2, 3, 4.\n   *\n   * It is possible that some cells have fewer parts than others. In this case\n   * LinePartIterator will \"fill up\" these cells with 0-length parts. IOW, LinePartIterator\n   * always returns the same number of parts for each column. Note that this is different\n   * from the way rows and columns are handled. LinePartIterator does @e not guarantee that\n   * the same number of columns will be returned for each row.\n   *\n   */\n  class LinePartIterator\n  {\n    const Descriptor* tablestart; //!< The 1st descriptor of the current table.\n    const Descriptor* rowdesc; //!< The Descriptor that contains the current row.\n    const char* rowstart; //!< Ptr to 1st character of current row within rowdesc->help.\n    const char* ptr; //!< Ptr to current part within the current row.\n    int col; //!< Index of current column.\n    int len; //!< Length of the current part (that ptr points at) in BYTES\n    int screenlen; //!< Length of the current part in screen columns (taking narrow/wide chars into account).\n    int max_line_in_block; //!< Greatest index of a line within the block. This is the number of \\\\v within the cell with the most \\\\vs.\n    int line_in_block; //!< Line index within the current cell of the current part.\n    int target_line_in_block; //!< Line index of the parts we should return to the user on this iteration.\n    bool hit_target_line; //!< Flag whether we encountered a part with line index target_line_in_block in the current cell.\n\n    /** \n     * @brief Determines the byte and character lengths of the part at @ref ptr and \n     * stores them in @ref len and @ref screenlen respectively.\n     */\n    void update_length()\n    {\n      screenlen = 0;\n      for (len = 0; ptr[len] != 0 && ptr[len] != '\\v' && ptr[len] != '\\t' && ptr[len] != '\\n'; ++len)\n      {\n        ++screenlen;\n        unsigned ch = (unsigned char) ptr[len];\n        if (ch > 0xC1) // everything <= 0xC1 (yes, even 0xC1 itself) is not a valid UTF-8 start byte\n        {\n          // int __builtin_clz (unsigned int x)\n          // Returns the number of leading 0-bits in x, starting at the most significant bit\n          unsigned mask = (unsigned) -1 >> __builtin_clz(ch ^ 0xff);\n          ch = ch & mask; // mask out length bits, we don't verify their correctness\n          while (((unsigned char) ptr[len + 1] ^ 0x80) <= 0x3F) // while next byte is continuation byte\n          {\n            ch = (ch << 6) ^ (unsigned char) ptr[len + 1] ^ 0x80; // add continuation to char code\n            ++len;\n          }\n          // ch is the decoded unicode code point\n          if (ch >= 0x1100 && isWideChar(ch)) // the test for 0x1100 is here to avoid the function call in the Latin case\n            ++screenlen;\n        }\n      }\n    }\n\n  public:\n    //! @brief Creates an iterator for @c usage.\n    LinePartIterator(const Descriptor usage[]) :\n        tablestart(usage), rowdesc(0), rowstart(0), ptr(0), col(-1), len(0), max_line_in_block(0), line_in_block(0),\n        target_line_in_block(0), hit_target_line(true)\n    {\n    }\n\n    /**\n     * @brief Moves iteration to the next table (if any). Has to be called once on a new\n     * LinePartIterator to move to the 1st table.\n     * @retval false if moving to next table failed because no further table exists.\n     */\n    bool nextTable()\n    {\n      // If this is NOT the first time nextTable() is called after the constructor,\n      // then skip to the next table break (i.e. a Descriptor with help == 0)\n      if (rowdesc != 0)\n      {\n        while (tablestart->help != 0 && tablestart->shortopt != 0)\n          ++tablestart;\n      }\n\n      // Find the next table after the break (if any)\n      while (tablestart->help == 0 && tablestart->shortopt != 0)\n        ++tablestart;\n\n      restartTable();\n      return rowstart != 0;\n    }\n\n    /**\n     * @brief Reset iteration to the beginning of the current table.\n     */\n    void restartTable()\n    {\n      rowdesc = tablestart;\n      rowstart = tablestart->help;\n      ptr = 0;\n    }\n\n    /**\n     * @brief Moves iteration to the next row (if any). Has to be called once after each call to\n     * @ref nextTable() to move to the 1st row of the table.\n     * @retval false if moving to next row failed because no further row exists.\n     */\n    bool nextRow()\n    {\n      if (ptr == 0)\n      {\n        restartRow();\n        return rowstart != 0;\n      }\n\n      while (*ptr != 0 && *ptr != '\\n')\n        ++ptr;\n\n      if (*ptr == 0)\n      {\n        if ((rowdesc + 1)->help == 0) // table break\n          return false;\n\n        ++rowdesc;\n        rowstart = rowdesc->help;\n      }\n      else // if (*ptr == '\\n')\n      {\n        rowstart = ptr + 1;\n      }\n\n      restartRow();\n      return true;\n    }\n\n    /**\n     * @brief Reset iteration to the beginning of the current row.\n     */\n    void restartRow()\n    {\n      ptr = rowstart;\n      col = -1;\n      len = 0;\n      screenlen = 0;\n      max_line_in_block = 0;\n      line_in_block = 0;\n      target_line_in_block = 0;\n      hit_target_line = true;\n    }\n\n    /**\n     * @brief Moves iteration to the next part (if any). Has to be called once after each call to\n     * @ref nextRow() to move to the 1st part of the row.\n     * @retval false if moving to next part failed because no further part exists.\n     *\n     * See @ref LinePartIterator for details about the iteration.\n     */\n    bool next()\n    {\n      if (ptr == 0)\n        return false;\n\n      if (col == -1)\n      {\n        col = 0;\n        update_length();\n        return true;\n      }\n\n      ptr += len;\n      while (true)\n      {\n        switch (*ptr)\n        {\n          case '\\v':\n            upmax(max_line_in_block, ++line_in_block);\n            ++ptr;\n            break;\n          case '\\t':\n            if (!hit_target_line) // if previous column did not have the targetline\n            { // then \"insert\" a 0-length part\n              update_length();\n              hit_target_line = true;\n              return true;\n            }\n\n            hit_target_line = false;\n            line_in_block = 0;\n            ++col;\n            ++ptr;\n            break;\n          case 0:\n          case '\\n':\n            if (!hit_target_line) // if previous column did not have the targetline\n            { // then \"insert\" a 0-length part\n              update_length();\n              hit_target_line = true;\n              return true;\n            }\n\n            if (++target_line_in_block > max_line_in_block)\n            {\n              update_length();\n              return false;\n            }\n\n            hit_target_line = false;\n            line_in_block = 0;\n            col = 0;\n            ptr = rowstart;\n            continue;\n          default:\n            ++ptr;\n            continue;\n        } // switch\n\n        if (line_in_block == target_line_in_block)\n        {\n          update_length();\n          hit_target_line = true;\n          return true;\n        }\n      } // while\n    }\n\n    /**\n     * @brief Returns the index (counting from 0) of the column in which\n     * the part pointed to by @ref data() is located.\n     */\n    int column()\n    {\n      return col;\n    }\n\n    /**\n     * @brief Returns the index (counting from 0) of the line within the current column\n     * this part belongs to.\n     */\n    int line()\n    {\n      return target_line_in_block; // NOT line_in_block !!! It would be wrong if !hit_target_line\n    }\n\n    /**\n     * @brief Returns the length of the part pointed to by @ref data() in raw chars (not UTF-8 characters).\n     */\n    int length()\n    {\n      return len;\n    }\n\n    /**\n     * @brief Returns the width in screen columns of the part pointed to by @ref data().\n     * Takes multi-byte UTF-8 sequences and wide characters into account.\n     */\n    int screenLength()\n    {\n      return screenlen;\n    }\n\n    /**\n     * @brief Returns the current part of the iteration.\n     */\n    const char* data()\n    {\n      return ptr;\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Takes input and line wraps it, writing out one line at a time so that\n   * it can be interleaved with output from other columns.\n   *\n   * The LineWrapper is used to handle the last column of each table as well as interjections.\n   * The LineWrapper is called once for each line of output. If the data given to it fits\n   * into the designated width of the last column it is simply written out. If there\n   * is too much data, an appropriate split point is located and only the data up to this\n   * split point is written out. The rest of the data is queued for the next line.\n   * That way the last column can be line wrapped and interleaved with data from\n   * other columns. The following example makes this clearer:\n   * @code\n   * Column 1,1    Column 2,1     This is a long text\n   * Column 1,2    Column 2,2     that does not fit into\n   *                              a single line.\n   * @endcode\n   *\n   * The difficulty in producing this output is that the whole string\n   * \"This is a long text that does not fit into a single line\" is the\n   * 1st and only part of column 3. In order to produce the above\n   * output the string must be output piecemeal, interleaved with\n   * the data from the other columns.\n   */\n  class LineWrapper\n  {\n    static const int bufmask = 15; //!< Must be a power of 2 minus 1.\n    /**\n     * @brief Ring buffer for length component of pair (data, length).\n     */\n    int lenbuf[bufmask + 1];\n    /**\n     * @brief Ring buffer for data component of pair (data, length).\n     */\n    const char* datbuf[bufmask + 1];\n    /**\n     * @brief The indentation of the column to which the LineBuffer outputs. LineBuffer\n     * assumes that the indentation has already been written when @ref process()\n     * is called, so this value is only used when a buffer flush requires writing\n     * additional lines of output.\n     */\n    int x;\n    /**\n     * @brief The width of the column to line wrap.\n     */\n    int width;\n    int head; //!< @brief index for next write\n    int tail; //!< @brief index for next read - 1 (i.e. increment tail BEFORE read)\n\n    /**\n     * @brief Multiple methods of LineWrapper may decide to flush part of the buffer to\n     * free up space. The contract of process() says that only 1 line is output. So\n     * this variable is used to track whether something has output a line. It is\n     * reset at the beginning of process() and checked at the end to decide if\n     * output has already occurred or is still needed.\n     */\n    bool wrote_something;\n\n    bool buf_empty()\n    {\n      return ((tail + 1) & bufmask) == head;\n    }\n\n    bool buf_full()\n    {\n      return tail == head;\n    }\n\n    void buf_store(const char* data, int len)\n    {\n      lenbuf[head] = len;\n      datbuf[head] = data;\n      head = (head + 1) & bufmask;\n    }\n\n    //! @brief Call BEFORE reading ...buf[tail].\n    void buf_next()\n    {\n      tail = (tail + 1) & bufmask;\n    }\n\n    /**\n     * @brief Writes (data,len) into the ring buffer. If the buffer is full, a single line\n     * is flushed out of the buffer into @c write.\n     */\n    void output(IStringWriter& write, const char* data, int len)\n    {\n      if (buf_full())\n        write_one_line(write);\n\n      buf_store(data, len);\n    }\n\n    /**\n     * @brief Writes a single line of output from the buffer to @c write.\n     */\n    void write_one_line(IStringWriter& write)\n    {\n      if (wrote_something) // if we already wrote something, we need to start a new line\n      {\n        write(\"\\n\", 1);\n        int _ = 0;\n        indent(write, _, x);\n      }\n\n      if (!buf_empty())\n      {\n        buf_next();\n        write(datbuf[tail], lenbuf[tail]);\n      }\n\n      wrote_something = true;\n    }\n  public:\n\n    /**\n     * @brief Writes out all remaining data from the LineWrapper using @c write.\n     * Unlike @ref process() this method indents all lines including the first and\n     * will output a \\\\n at the end (but only if something has been written).\n     */\n    void flush(IStringWriter& write)\n    {\n      if (buf_empty())\n        return;\n      int _ = 0;\n      indent(write, _, x);\n      wrote_something = false;\n      while (!buf_empty())\n        write_one_line(write);\n      write(\"\\n\", 1);\n    }\n\n    /**\n     * @brief Process, wrap and output the next piece of data.\n     *\n     * process() will output at least one line of output. This is not necessarily\n     * the @c data passed in. It may be data queued from a prior call to process().\n     * If the internal buffer is full, more than 1 line will be output.\n     *\n     * process() assumes that the a proper amount of indentation has already been\n     * output. It won't write any further indentation before the 1st line. If\n     * more than 1 line is written due to buffer constraints, the lines following\n     * the first will be indented by this method, though.\n     *\n     * No \\\\n is written by this method after the last line that is written.\n     *\n     * @param write where to write the data.\n     * @param data the new chunk of data to write.\n     * @param len the length of the chunk of data to write.\n     */\n    void process(IStringWriter& write, const char* data, int len)\n    {\n      wrote_something = false;\n\n      while (len > 0)\n      {\n        if (len <= width) // quick test that works because utf8width <= len (all wide chars have at least 2 bytes)\n        {\n          output(write, data, len);\n          len = 0;\n        }\n        else // if (len > width)  it's possible (but not guaranteed) that utf8len > width\n        {\n          int utf8width = 0;\n          int maxi = 0;\n          while (maxi < len && utf8width < width)\n          {\n            int charbytes = 1;\n            unsigned ch = (unsigned char) data[maxi];\n            if (ch > 0xC1) // everything <= 0xC1 (yes, even 0xC1 itself) is not a valid UTF-8 start byte\n            {\n              // int __builtin_clz (unsigned int x)\n              // Returns the number of leading 0-bits in x, starting at the most significant bit\n              unsigned mask = (unsigned) -1 >> __builtin_clz(ch ^ 0xff);\n              ch = ch & mask; // mask out length bits, we don't verify their correctness\n              while ((maxi + charbytes < len) && //\n                  (((unsigned char) data[maxi + charbytes] ^ 0x80) <= 0x3F)) // while next byte is continuation byte\n              {\n                ch = (ch << 6) ^ (unsigned char) data[maxi + charbytes] ^ 0x80; // add continuation to char code\n                ++charbytes;\n              }\n              // ch is the decoded unicode code point\n              if (ch >= 0x1100 && isWideChar(ch)) // the test for 0x1100 is here to avoid the function call in the Latin case\n              {\n                if (utf8width + 2 > width)\n                  break;\n                ++utf8width;\n              }\n            }\n            ++utf8width;\n            maxi += charbytes;\n          }\n\n          // data[maxi-1] is the last byte of the UTF-8 sequence of the last character that fits\n          // onto the 1st line. If maxi == len, all characters fit on the line.\n\n          if (maxi == len)\n          {\n            output(write, data, len);\n            len = 0;\n          }\n          else // if (maxi < len)  at least 1 character (data[maxi] that is) doesn't fit on the line\n          {\n            int i;\n            for (i = maxi; i >= 0; --i)\n              if (data[i] == ' ')\n                break;\n\n            if (i >= 0)\n            {\n              output(write, data, i);\n              data += i + 1;\n              len -= i + 1;\n            }\n            else // did not find a space to split at => split before data[maxi]\n            { // data[maxi] is always the beginning of a character, never a continuation byte\n              output(write, data, maxi);\n              data += maxi;\n              len -= maxi;\n            }\n          }\n        }\n      }\n      if (!wrote_something) // if we didn't already write something to make space in the buffer\n        write_one_line(write); // write at most one line of actual output\n    }\n\n    /**\n     * @brief Constructs a LineWrapper that wraps its output to fit into\n     * screen columns @c x1 (incl.) to @c x2 (excl.).\n     *\n     * @c x1 gives the indentation LineWrapper uses if it needs to indent.\n     */\n    LineWrapper(int x1, int x2) :\n        x(x1), width(x2 - x1), head(0), tail(bufmask)\n    {\n      if (width < 2) // because of wide characters we need at least width 2 or the code breaks\n        width = 2;\n    }\n  };\n\n  /**\n   * @internal\n   * @brief This is the implementation that is shared between all printUsage() templates.\n   * Because all printUsage() templates share this implementation, there is no template bloat.\n   */\n  static void printUsage(IStringWriter& write, const Descriptor usage[], int width = 80, //\n                         int last_column_min_percent = 50, int last_column_own_line_max_percent = 75)\n  {\n    if (width < 1) // protect against nonsense values\n      width = 80;\n\n    if (width > 10000) // protect against overflow in the following computation\n      width = 10000;\n\n    int last_column_min_width = ((width * last_column_min_percent) + 50) / 100;\n    int last_column_own_line_max_width = ((width * last_column_own_line_max_percent) + 50) / 100;\n    if (last_column_own_line_max_width == 0)\n      last_column_own_line_max_width = 1;\n\n    LinePartIterator part(usage);\n    while (part.nextTable())\n    {\n\n      /***************** Determine column widths *******************************/\n\n      const int maxcolumns = 8; // 8 columns are enough for everyone\n      int col_width[maxcolumns];\n      int lastcolumn;\n      int leftwidth;\n      int overlong_column_threshold = 10000;\n      do\n      {\n        lastcolumn = 0;\n        for (int i = 0; i < maxcolumns; ++i)\n          col_width[i] = 0;\n\n        part.restartTable();\n        while (part.nextRow())\n        {\n          while (part.next())\n          {\n            if (part.column() < maxcolumns)\n            {\n              upmax(lastcolumn, part.column());\n              if (part.screenLength() < overlong_column_threshold)\n                // We don't let rows that don't use table separators (\\t or \\v) influence\n                // the width of column 0. This allows the user to interject section headers\n                // or explanatory paragraphs that do not participate in the table layout.\n                if (part.column() > 0 || part.line() > 0 || part.data()[part.length()] == '\\t'\n                    || part.data()[part.length()] == '\\v')\n                  upmax(col_width[part.column()], part.screenLength());\n            }\n          }\n        }\n\n        /*\n         * If the last column doesn't fit on the same\n         * line as the other columns, we can fix that by starting it on its own line.\n         * However we can't do this for any of the columns 0..lastcolumn-1.\n         * If their sum exceeds the maximum width we try to fix this by iteratively\n         * ignoring the widest line parts in the width determination until\n         * we arrive at a series of column widths that fit into one line.\n         * The result is a layout where everything is nicely formatted\n         * except for a few overlong fragments.\n         * */\n\n        leftwidth = 0;\n        overlong_column_threshold = 0;\n        for (int i = 0; i < lastcolumn; ++i)\n        {\n          leftwidth += col_width[i];\n          upmax(overlong_column_threshold, col_width[i]);\n        }\n\n      } while (leftwidth > width);\n\n      /**************** Determine tab stops and last column handling **********************/\n\n      int tabstop[maxcolumns];\n      tabstop[0] = 0;\n      for (int i = 1; i < maxcolumns; ++i)\n        tabstop[i] = tabstop[i - 1] + col_width[i - 1];\n\n      int rightwidth = width - tabstop[lastcolumn];\n      bool print_last_column_on_own_line = false;\n      if (rightwidth < last_column_min_width &&  // if we don't have the minimum requested width for the last column\n            ( col_width[lastcolumn] == 0 ||      // and all last columns are > overlong_column_threshold\n              rightwidth < col_width[lastcolumn] // or there is at least one last column that requires more than the space available\n            )\n          )\n      {\n        print_last_column_on_own_line = true;\n        rightwidth = last_column_own_line_max_width;\n      }\n\n      // If lastcolumn == 0 we must disable print_last_column_on_own_line because\n      // otherwise 2 copies of the last (and only) column would be output.\n      // Actually this is just defensive programming. It is currently not\n      // possible that lastcolumn==0 and print_last_column_on_own_line==true\n      // at the same time, because lastcolumn==0 => tabstop[lastcolumn] == 0 =>\n      // rightwidth==width => rightwidth>=last_column_min_width  (unless someone passes\n      // a bullshit value >100 for last_column_min_percent) => the above if condition\n      // is false => print_last_column_on_own_line==false\n      if (lastcolumn == 0)\n        print_last_column_on_own_line = false;\n\n      LineWrapper lastColumnLineWrapper(width - rightwidth, width);\n      LineWrapper interjectionLineWrapper(0, width);\n\n      part.restartTable();\n\n      /***************** Print out all rows of the table *************************************/\n\n      while (part.nextRow())\n      {\n        int x = -1;\n        while (part.next())\n        {\n          if (part.column() > lastcolumn)\n            continue; // drop excess columns (can happen if lastcolumn == maxcolumns-1)\n\n          if (part.column() == 0)\n          {\n            if (x >= 0)\n              write(\"\\n\", 1);\n            x = 0;\n          }\n\n          indent(write, x, tabstop[part.column()]);\n\n          if ((part.column() < lastcolumn)\n              && (part.column() > 0 || part.line() > 0 || part.data()[part.length()] == '\\t'\n                  || part.data()[part.length()] == '\\v'))\n          {\n            write(part.data(), part.length());\n            x += part.screenLength();\n          }\n          else // either part.column() == lastcolumn or we are in the special case of\n               // an interjection that doesn't contain \\v or \\t\n          {\n            // NOTE: This code block is not necessarily executed for\n            // each line, because some rows may have fewer columns.\n\n            LineWrapper& lineWrapper = (part.column() == 0) ? interjectionLineWrapper : lastColumnLineWrapper;\n\n            if (!print_last_column_on_own_line || part.column() != lastcolumn)\n              lineWrapper.process(write, part.data(), part.length());\n          }\n        } // while\n\n        if (print_last_column_on_own_line)\n        {\n          part.restartRow();\n          while (part.next())\n          {\n            if (part.column() == lastcolumn)\n            {\n              write(\"\\n\", 1);\n              int _ = 0;\n              indent(write, _, width - rightwidth);\n              lastColumnLineWrapper.process(write, part.data(), part.length());\n            }\n          }\n        }\n\n        write(\"\\n\", 1);\n        lastColumnLineWrapper.flush(write);\n        interjectionLineWrapper.flush(write);\n      }\n    }\n  }\n\n}\n;\n\n/**\n * @brief Outputs a nicely formatted usage string with support for multi-column formatting\n * and line-wrapping.\n *\n * printUsage() takes the @c help texts of a Descriptor[] array and formats them into\n * a usage message, wrapping lines to achieve the desired output width.\n *\n * <b>Table formatting:</b>\n *\n * Aside from plain strings which are simply line-wrapped, the usage may contain tables. Tables\n * are used to align elements in the output.\n *\n * @code\n * // Without a table. The explanatory texts are not aligned.\n * -c, --create  |Creates something.\n * -k, --kill  |Destroys something.\n *\n * // With table formatting. The explanatory texts are aligned.\n * -c, --create  |Creates something.\n * -k, --kill    |Destroys something.\n * @endcode\n *\n * Table formatting removes the need to pad help texts manually with spaces to achieve\n * alignment. To create a table, simply insert \\\\t (tab) characters to separate the cells\n * within a row.\n *\n * @code\n * const option::Descriptor usage[] = {\n * {..., \"-c, --create  \\tCreates something.\" },\n * {..., \"-k, --kill  \\tDestroys something.\" }, ...\n * @endcode\n *\n * Note that you must include the minimum amount of space desired between cells yourself.\n * Table formatting will insert further spaces as needed to achieve alignment.\n *\n * You can insert line breaks within cells by using \\\\v (vertical tab).\n *\n * @code\n * const option::Descriptor usage[] = {\n * {..., \"-c,\\v--create  \\tCreates\\vsomething.\" },\n * {..., \"-k,\\v--kill  \\tDestroys\\vsomething.\" }, ...\n *\n * // results in\n *\n * -c,       Creates\n * --create  something.\n * -k,       Destroys\n * --kill    something.\n * @endcode\n *\n * You can mix lines that do not use \\\\t or \\\\v with those that do. The plain\n * lines will not mess up the table layout. Alignment of the table columns will\n * be maintained even across these interjections.\n *\n * @code\n * const option::Descriptor usage[] = {\n * {..., \"-c, --create  \\tCreates something.\" },\n * {..., \"----------------------------------\" },\n * {..., \"-k, --kill  \\tDestroys something.\" }, ...\n *\n * // results in\n *\n * -c, --create  Creates something.\n * ----------------------------------\n * -k, --kill    Destroys something.\n * @endcode\n *\n * You can have multiple tables within the same usage whose columns are\n * aligned independently. Simply insert a dummy Descriptor with @c help==0.\n *\n * @code\n * const option::Descriptor usage[] = {\n * {..., \"Long options:\" },\n * {..., \"--very-long-option  \\tDoes something long.\" },\n * {..., \"--ultra-super-mega-long-option  \\tTakes forever to complete.\" },\n * {..., 0 }, // ---------- table break -----------\n * {..., \"Short options:\" },\n * {..., \"-s  \\tShort.\" },\n * {..., \"-q  \\tQuick.\" }, ...\n *\n * // results in\n *\n * Long options:\n * --very-long-option              Does something long.\n * --ultra-super-mega-long-option  Takes forever to complete.\n * Short options:\n * -s  Short.\n * -q  Quick.\n *\n * // Without the table break it would be\n *\n * Long options:\n * --very-long-option              Does something long.\n * --ultra-super-mega-long-option  Takes forever to complete.\n * Short options:\n * -s                              Short.\n * -q                              Quick.\n * @endcode\n *\n * <b>Output methods:</b>\n *\n * Because TheLeanMeanC++Option parser is freestanding, you have to provide the means for\n * output in the first argument(s) to printUsage(). Because printUsage() is implemented as\n * a set of template functions, you have great flexibility in your choice of output\n * method. The following example demonstrates typical uses. Anything that's similar enough\n * will work.\n *\n * @code\n * #include <unistd.h>  // write()\n * #include <iostream>  // cout\n * #include <sstream>   // ostringstream\n * #include <cstdio>    // fwrite()\n * using namespace std;\n *\n * void my_write(const char* str, int size) {\n *   fwrite(str, size, 1, stdout);\n * }\n *\n * struct MyWriter {\n *   void write(const char* buf, size_t size) const {\n *      fwrite(str, size, 1, stdout);\n *   }\n * };\n *\n * struct MyWriteFunctor {\n *   void operator()(const char* buf, size_t size) {\n *      fwrite(str, size, 1, stdout);\n *   }\n * };\n * ...\n * printUsage(my_write, usage);    // custom write function\n * printUsage(MyWriter(), usage);  // temporary of a custom class\n * MyWriter writer;\n * printUsage(writer, usage);      // custom class object\n * MyWriteFunctor wfunctor;\n * printUsage(&wfunctor, usage);   // custom functor\n * printUsage(write, 1, usage);    // write() to file descriptor 1\n * printUsage(cout, usage);        // an ostream&\n * printUsage(fwrite, stdout, usage);  // fwrite() to stdout\n * ostringstream sstr;\n * printUsage(sstr, usage);        // an ostringstream&\n *\n * @endcode\n *\n * @par Notes:\n * @li the @c write() method of a class that is to be passed as a temporary\n *     as @c MyWriter() is in the example, must be a @c const method, because\n *     temporary objects are passed as const reference. This only applies to\n *     temporary objects that are created and destroyed in the same statement.\n *     If you create an object like @c writer in the example, this restriction\n *     does not apply.\n * @li a functor like @c MyWriteFunctor in the example must be passed as a pointer.\n *     This differs from the way functors are passed to e.g. the STL algorithms.\n * @li All printUsage() templates are tiny wrappers around a shared non-template implementation.\n *     So there's no penalty for using different versions in the same program.\n * @li printUsage() always interprets Descriptor::help as UTF-8 and always produces UTF-8-encoded\n *     output. If your system uses a different charset, you must do your own conversion. You\n *     may also need to change the font of the console to see non-ASCII characters properly.\n *     This is particularly true for Windows.\n * @li @b Security @b warning: Do not insert untrusted strings (such as user-supplied arguments)\n *     into the usage. printUsage() has no protection against malicious UTF-8 sequences.\n *\n * @param prn The output method to use. See the examples above.\n * @param usage the Descriptor[] array whose @c help texts will be formatted.\n * @param width the maximum number of characters per output line. Note that this number is\n *        in actual characters, not bytes. printUsage() supports UTF-8 in @c help and will\n *        count multi-byte UTF-8 sequences properly. Asian wide characters are counted\n *        as 2 characters.\n * @param last_column_min_percent (0-100) The minimum percentage of @c width that should be available\n *        for the last column (which typically contains the textual explanation of an option).\n *        If less space is available, the last column will be printed on its own line, indented\n *        according to @c last_column_own_line_max_percent.\n * @param last_column_own_line_max_percent (0-100) If the last column is printed on its own line due to\n *        less than @c last_column_min_percent of the width being available, then only\n *        @c last_column_own_line_max_percent of the extra line(s) will be used for the\n *        last column's text. This ensures an indentation. See example below.\n *\n * @code\n * // width=20, last_column_min_percent=50 (i.e. last col. min. width=10)\n * --3456789 1234567890\n *           1234567890\n *\n * // width=20, last_column_min_percent=75 (i.e. last col. min. width=15)\n * // last_column_own_line_max_percent=75\n * --3456789\n *      123456789012345\n *      67890\n *\n * // width=20, last_column_min_percent=75 (i.e. last col. min. width=15)\n * // last_column_own_line_max_percent=33 (i.e. max. 5)\n * --3456789\n *                12345\n *                67890\n *                12345\n *                67890\n * @endcode\n */\ntemplate<typename OStream>\nvoid printUsage(OStream& prn, const Descriptor usage[], int width = 80, int last_column_min_percent = 50,\n                int last_column_own_line_max_percent = 75)\n{\n  PrintUsageImplementation::OStreamWriter<OStream> write(prn);\n  PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent);\n}\n\ntemplate<typename Function>\nvoid printUsage(Function* prn, const Descriptor usage[], int width = 80, int last_column_min_percent = 50,\n                int last_column_own_line_max_percent = 75)\n{\n  PrintUsageImplementation::FunctionWriter<Function> write(prn);\n  PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent);\n}\n\ntemplate<typename Temporary>\nvoid printUsage(const Temporary& prn, const Descriptor usage[], int width = 80, int last_column_min_percent = 50,\n                int last_column_own_line_max_percent = 75)\n{\n  PrintUsageImplementation::TemporaryWriter<Temporary> write(prn);\n  PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent);\n}\n\ntemplate<typename Syscall>\nvoid printUsage(Syscall* prn, int fd, const Descriptor usage[], int width = 80, int last_column_min_percent = 50,\n                int last_column_own_line_max_percent = 75)\n{\n  PrintUsageImplementation::SyscallWriter<Syscall> write(prn, fd);\n  PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent);\n}\n\ntemplate<typename Function, typename Stream>\nvoid printUsage(Function* prn, Stream* stream, const Descriptor usage[], int width = 80, int last_column_min_percent =\n                    50,\n                int last_column_own_line_max_percent = 75)\n{\n  PrintUsageImplementation::StreamWriter<Function, Stream> write(prn, stream);\n  PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent);\n}\n\n}\n// namespace option\n\n#endif /* OPTIONPARSER_H_ */\n"
  },
  {
    "path": "third-party/optionparser/printUsage.h",
    "content": "/**\n * @file\n * @brief Dummy file for documentation purposes.\n */\n\n/**\n * @brief @ref option::printUsage() formats the usage message with column alignment and line wrapping.\n */\nclass UsageMsg{\n  // Dummy file to get an entry for printUsage() in Classes.\n};\n"
  },
  {
    "path": "third-party/optionparser/testodr1.cc",
    "content": "/* Written 2012 by Matthias S. Benkmann\n *\n * The author hereby waives all copyright and related rights to the contents\n * of this example file (testodr1.cc) to the extent possible under the law.\n */\n\n/**\n * @file\n * @brief Test for multiple definition errors.\n *\n * @note\n * This program is for developing TLMC++OP. It is neither an example nor a functionality test.\n * Do not worry if it doesn't compile or run on your platform.\n *\n * @ref testodr1.cc and @ref testodr2.cc test optionparser.h for\n * violations of the one definition rule, both at compile-time and at\n * link-time. IOW, they test if optionparser.h can be included\n * multiple times as well as that multiple object files that include\n * it can be linked together.\n *\n */\n\n#include \"optionparser.h\"\n#include \"optionparser.h\" //intentionally included twice\n\n#include <cstdio>\n\nusing option::Option;\nusing option::Descriptor;\n\nextern const Descriptor usage[];\n\nextern bool bar(int argc, const char* argv[])\n{\n  printUsage(std::fwrite, stdout, usage);\n  option::Stats stats(usage, argc, argv);\n  option::Option buffer [stats.buffer_max];\n  option::Option options[stats.options_max];\n  option::Parser parse(usage, argc, argv, options, buffer);\n  return parse.error();\n}\n\nint main()\n{\n  Descriptor d = usage[0];\n  std::printf(\"%s\",d.shortopt);\n}\n\n\n\n"
  },
  {
    "path": "third-party/optionparser/testodr2.cc",
    "content": "/* Written 2012 by Matthias S. Benkmann\n *\n * The author hereby waives all copyright and related rights to the contents\n * of this example file (testodr2.cc) to the extent possible under the law.\n */\n\n/**\n * @file\n * @brief Test for multiple definition errors.\n * @copydoc testodr1.cc\n */\n\n#include \"optionparser.h\"\n\n#include <cstdio>\n\nusing option::Descriptor;\nusing option::Arg;\nenum OptionIndex {CREATE};\nenum OptionType {DISABLE, ENABLE, OTHER};\n\nextern const Descriptor usage[] = {\n   { CREATE, OTHER,\n     \"c\", \"create\",\n     Arg::None,\n     \"--create\\t\\t\\tTells the program to create something.\"\n   }\n };\n\nextern bool foo(int argc, const char* argv[])\n{\n  printUsage(std::fwrite, stdout, usage);\n  option::Stats stats(usage, argc, argv);\n  option::Option buffer [stats.buffer_max];\n  option::Option options[stats.options_max];\n  option::Parser parse(usage, argc, argv, options, buffer);\n  return parse.error();\n}\n\n"
  },
  {
    "path": "third-party/optionparser/testparse.cpp",
    "content": "/* Written 2012 by Matthias S. Benkmann\n *\n * The author hereby waives all copyright and related rights to the contents\n * of this example file (testparse.cpp) to the extent possible under the law.\n */\n\n/**\n * @file\n * @brief Test program for option::Stats and option::Parser.\n *\n * @note\n * This program is for developing TLMC++OP. It is neither an example nor a functionality test.\n * Do not worry if it doesn't compile or run on your platform.\n *\n */\n\n/**\n * @mainpage\n * @copydetails optionparser.h\n */\n\n#include <assert.h>\n#include <stdio.h>\n\n#include \"optionparser.h\"\n\nusing option::Option;\nusing option::Descriptor;\nusing option::Parser;\nusing option::Stats;\nusing option::ArgStatus;\n\nstruct Arg: public option::Arg\n{\n  static ArgStatus Required(const Option& option, bool)\n  {\n    return option.arg == 0 ? option::ARG_ILLEGAL : option::ARG_OK;\n  }\n  static ArgStatus Empty(const Option& option, bool)\n  {\n    return (option.arg == 0 || option.arg[0] == 0) ? option::ARG_OK : option::ARG_IGNORE;\n  }\n};\n\nchar* gettext(const char * msgid)\n{\n  return (char*) msgid;\n}\n\nconst Descriptor empty_usage[] = { { 0, 0, 0, 0, 0, 0 } };\n\nconst Descriptor minimal_usage[] = //\n    { { 0, 0, \"x\", \"\", Arg::None, 0 }, //\n      { 0, 0, 0, 0, 0, 0 } };\n\nconst Descriptor optional_usage[] = //\n    { { 0, 0, \"f\", \"\", Arg::Required, 0 }, //\n      { 0, 0, 0, 0, 0, 0 } };\n\nconst Descriptor gettext_usage[] = //\n    { { 0, 0, \"f\", \"\", Arg::Required, gettext(\"This is a test\") }, //\n      { 0, 0, 0, 0, 0, 0 } };\n\nenum OptionIndex\n{\n  UNKNOWN, FOO, VERBOSE, X, ABBREVIATE, EMPTY\n};\nenum OptionType\n{\n  UNUSED = 0, DISABLED = 1, ENABLED = 2\n};\n\nconst Descriptor multi_usage[] = //\n    { { UNKNOWN, 0, \"\", \"\", Arg::None, 0 }, // UNKNOWN option catch-all\n      { FOO, ENABLED, \"\", \"enable-foo\", Arg::None, 0 }, // FOO enable\n      { FOO, DISABLED, \"\", \"disable-foo\", Arg::None, 0 }, // FOO disable\n      { VERBOSE, 0, \"v\", \"verbose\", Arg::None, 0 }, // VERBOSE (counted option)\n      { X, 0, \"X\", \"X\", Arg::Required, 0 }, // -X<arg>, -X <arg>, -X=<arg>, --X=<arg>\n      { ABBREVIATE, 0, \"\", \"abbreviate-me\", Arg::None, 0 }, // ABBREVIATE\n      { EMPTY, 0, \"\", \"emptyarg\", Arg::Empty, 0 }, // EMPTY (ignores arguments that are not \"\")\n      { 0, 0, 0, 0, 0, 0 } };\n\nconst char* empty_args[] = { 0 };\nconst char* non_options[] = { \"1\", \"2\", \"3\", (const char*) -1 };\nconst char* unknown_option[] = { \"--unknown\", \"nonoption\", 0 };\nconst char* lone_minus[] = { \"-f\", \"-\", \"-\", 0 };\nconst char* lone_doubleminus[] = { \"--\", 0 };\n\n// NOTE: (const char*) -1 is used to cause a segfault should this element be dereferenced.\n// 0 is not used here, because the parser explicitly checks for 0 which could mask bugs.\n// If a number of arguments >= 0 is passed, the parser is supposed to honor that and never\n// dereference an element beyond the last.\nconst char* multi1[] =\n    { \"--enable-foo\", \"--unknown1\", \"-u\", \"-vX\", \"xyzzy\", \"--\", \"--strangefilename\", (const char*) -1 };\nconst char* multi2[] = { \"-vvXfoo\", \"-X\", \"bar\", \"-X=foobar\", \"-X\", \"\", \"--disable-foo\", \"-v\", (const char*) -1 };\nconst char* multi3[] = { \"-abbr\", \"-abb\", \"--emptyarg\", \"-verbose\", \"--emptyarg\", \"\", \"--emptyarg=\", \"nonoption1\",\n                         \"nonoption2\", (const char*) -1 };\n\nconst char* illegal[] = { \"-X\", 0 };\nconst char* reorder[] = { \"-X\", \"--\", \"-\", \"-X\", \"--\", \"foo\", \"-v\", \"--\", \"bar\", \"--\", 0 };\nconst char* reorder2[] = { \"-X\", \"--\", \"-\", \"-X\", \"--\", \"-\", 0 };\n\nint count(const char** args)\n{\n  for (int c = 0;; ++c)\n    if (args[c] == (const char*) -1)\n      return c;\n}\n\nbool eq(const char* s1, const char* s2)\n{\n  if (s1 == s2)\n    return true;\n\n  if (s1 == 0 || s2 == 0)\n    return false;\n\n  while (*s1 != 0 && *s2 != 0)\n  {\n    ++s1;\n    ++s2;\n  }\n\n  return *s1 == *s2;\n}\n\nint main()\n{\n  {\n    Stats stats(empty_usage, -1, empty_args);\n    stats.add(empty_usage, 0, empty_args);\n    assert(stats.buffer_max == 1);\n    assert(stats.options_max == 1);\n    Option buffer[stats.buffer_max];\n    Option options[stats.options_max];\n    Parser parse(empty_usage, 99, empty_args, options, buffer);\n    parse.parse(empty_usage, -1, empty_args, options, buffer);\n    assert(parse.optionsCount() == 0);\n    assert(parse.nonOptionsCount() == 0);\n    assert(!buffer[0]);\n    assert(!options[0]);\n    assert(buffer[0].count()==0);\n    assert(parse.nonOptions()==0);\n\n    stats.add(empty_usage, 3, non_options);\n    assert(stats.buffer_max == 1);\n    assert(stats.options_max == 1);\n    parse.parse(empty_usage, 3, non_options, options, buffer);\n    assert(parse.optionsCount() == 0);\n    assert(parse.nonOptionsCount() == 3);\n    assert(!buffer[0]);\n    assert(!options[0]);\n    assert(parse.nonOptions()==&non_options[0]);\n\n    stats.add(minimal_usage, -1, unknown_option);\n    assert(stats.buffer_max == 1);\n    assert(stats.options_max == 2);\n    parse.parse(minimal_usage, -1, unknown_option, options, buffer);\n    assert(parse.optionsCount() == 0);\n    assert(parse.nonOptionsCount() == 1);\n    assert(!buffer[0]);\n    assert(!options[0]);\n    assert(parse.nonOptions()==&unknown_option[1]);\n  }\n  {\n    Stats stats(gettext_usage, -1, lone_minus);\n    Stats stats2;\n    stats2.add(gettext_usage, -1, lone_minus);\n    assert(stats.buffer_max == 2);\n    assert(stats.options_max == 2);\n    assert(stats2.buffer_max == 2);\n    assert(stats2.options_max == 2);\n    Option buffer[stats.buffer_max];\n    Option options[stats.options_max];\n    Parser parse;\n    parse.parse(gettext_usage, -1, lone_minus, options, buffer);\n    assert(parse.optionsCount() == 1);\n    assert(parse.nonOptionsCount() == 1);\n    assert(parse.nonOptions()==&lone_minus[2]);\n    assert(options[0]);\n    assert(buffer[0]);\n    assert(options[0].count()==1);\n    assert(options[0].isFirst());\n    assert(options[0].isLast());\n    assert(options[0].first() == options[0]);\n    assert(options[0].last() == options[0]);\n    assert(options[0].prevwrap() == &options[0]);\n    assert(options[0].nextwrap() == &options[0]);\n    assert(options[0].prev() == 0);\n    assert(options[0].next() == 0);\n    assert(options[0].desc == &gettext_usage[0]);\n    assert(eq(options[0].name, \"f\"));\n    assert(eq(options[0].arg, \"-\"));\n  }\n  {\n    Stats stats(optional_usage, -1, lone_minus);\n    Stats stats2;\n    stats2.add(optional_usage, -1, lone_minus);\n    assert(stats.buffer_max == 2);\n    assert(stats.options_max == 2);\n    assert(stats2.buffer_max == 2);\n    assert(stats2.options_max == 2);\n    Option buffer[stats.buffer_max];\n    Option options[stats.options_max];\n    Parser parse;\n    parse.parse(optional_usage, -1, lone_minus, options, buffer);\n    assert(parse.optionsCount() == 1);\n    assert(parse.nonOptionsCount() == 1);\n    assert(parse.nonOptions()==&lone_minus[2]);\n    assert(options[0]);\n    assert(buffer[0]);\n    assert(options[0].count()==1);\n    assert(options[0].isFirst());\n    assert(options[0].isLast());\n    assert(options[0].first() == options[0]);\n    assert(options[0].last() == options[0]);\n    assert(options[0].prevwrap() == &options[0]);\n    assert(options[0].nextwrap() == &options[0]);\n    assert(options[0].prev() == 0);\n    assert(options[0].next() == 0);\n    assert(options[0].desc == &optional_usage[0]);\n    assert(eq(options[0].name, \"f\"));\n    assert(eq(options[0].arg, \"-\"));\n  }\n  {\n    Stats stats;\n    stats.add(minimal_usage, -1, lone_doubleminus);\n    assert(stats.buffer_max == 1);\n    assert(stats.options_max == 2);\n    Option buffer[stats.buffer_max];\n    Option options[stats.options_max];\n    Parser parse(minimal_usage, -1, lone_doubleminus, options, buffer);\n    assert(parse.optionsCount() == 0);\n    assert(parse.nonOptionsCount() == 0);\n    assert(!buffer[0]);\n    assert(!options[0]);\n    assert(parse.nonOptions()==0);\n  }\n  {\n    Stats stats;\n    stats.add(multi_usage, count(multi1), multi1, 4, true);\n    assert(stats.buffer_max == 6);\n    assert(stats.options_max == 7);\n    stats.add(multi_usage, count(multi2), multi2, 4, true);\n    assert(stats.buffer_max == 14);\n    assert(stats.options_max == 7);\n    stats.add(multi_usage, count(multi3), multi3, 4, true);\n    assert(stats.buffer_max == 22);\n    assert(stats.options_max == 7);\n    Option buffer[stats.buffer_max];\n    Option options[stats.options_max];\n    assert(options[FOO].last()->type() == UNUSED);\n    assert(options[ABBREVIATE].count()==0);\n    Parser parse;\n    assert(!parse.error());\n\n    parse.parse(multi_usage, count(multi1), multi1, options, buffer, 4, true);\n    assert(!parse.error());\n    assert(parse.optionsCount() == 5);\n    assert(parse.nonOptionsCount() == 1);\n    assert(eq(parse.nonOptions()[0],\"--strangefilename\"));\n    assert(options[FOO].last()->type() == ENABLED);\n    assert(eq(options[FOO].last()->name, \"--enable-foo\"));\n    assert(options[FOO].last()->arg == 0);\n    assert(options[UNKNOWN].count() == 2);\n    assert(eq(options[UNKNOWN].first()->name,\"--unknown1\"));\n    assert(eq(options[UNKNOWN].last()->name,\"u\"));\n    assert(options[UNKNOWN].first()->arg == 0);\n    assert(options[UNKNOWN].last()->arg == 0);\n    assert(options[VERBOSE].count()==1);\n    assert(options[VERBOSE].arg==0);\n    assert(options[VERBOSE].name[0] == 'v' && options[VERBOSE].namelen == 1);\n    assert(eq(options[X].arg,\"xyzzy\"));\n    assert(eq(options[X].name,\"X\"));\n\n    parse.parse(multi_usage, count(multi2), multi2, options, buffer, 4, true);\n    assert(!parse.error());\n    assert(parse.optionsCount() == 13);\n    assert(parse.nonOptionsCount() == 1);\n    assert(eq(parse.nonOptions()[0],\"--strangefilename\"));\n    assert(options[FOO].last()->type() == DISABLED);\n    assert(options[FOO].last()->arg == 0);\n    assert(options[UNKNOWN].count() == 2);\n    assert(eq(options[UNKNOWN].first()->name,\"--unknown1\"));\n    assert(eq(options[UNKNOWN].last()->name,\"u\"));\n    assert(options[VERBOSE].count()==4);\n    assert(options[X].count()==5);\n    const char* Xargs[] = { \"xyzzy\", \"foo\", \"bar\", \"foobar\", \"\", \"sentinel\" };\n    const char** Xarg = &Xargs[0];\n    for (Option* Xiter = options[X]; Xiter != 0; Xiter = Xiter->next())\n      assert(eq(Xiter->arg, *Xarg++));\n\n    assert(!options[ABBREVIATE]);\n    parse.parse(multi_usage, count(multi3), multi3, options, buffer, 4, true);\n    assert(!parse.error());\n    assert(parse.optionsCount() == 21);\n    assert(parse.nonOptionsCount() == 2);\n    assert(eq(parse.nonOptions()[0],\"nonoption1\"));\n    assert(eq(parse.nonOptions()[1],\"nonoption2\"));\n    assert(options[ABBREVIATE]);\n    assert(options[EMPTY].count()==3);\n    assert(options[EMPTY].first()->arg==0);\n    assert(eq(options[EMPTY].last()->arg,\"\"));\n    assert(eq(options[EMPTY].last()->prev()->arg,\"\"));\n    assert(options[FOO].last()->type() == DISABLED);\n    assert(options[UNKNOWN].count() == 5);\n    assert(eq(options[UNKNOWN].first()->name,\"--unknown1\"));\n    assert(options[UNKNOWN].first()->arg == 0);\n    assert(eq(options[UNKNOWN].last()->name,\"b\"));\n    assert(options[VERBOSE].count()==5);\n    assert(options[X].count()==5);\n    Xarg = &Xargs[0];\n    for (Option* Xiter = options[X]; Xiter != 0; Xiter = Xiter->next())\n      assert(eq(Xiter->arg, *Xarg++));\n\n    for (Option* opt = buffer[0]; *opt; ++opt)\n      if (opt->desc->check_arg != Arg::Required && opt->desc->check_arg != Arg::Empty)\n        assert(opt->arg == 0);\n  }\n  {\n    Option buffer[2];\n    Option options[20];\n    Parser parse;\n    assert(!parse.error());\n    parse.parse(multi_usage, -1, illegal, options, buffer, 0, false, 2);\n    assert(parse.error());\n  }\n  {\n    Stats stats(multi_usage, count(multi3), multi3, 0, true);\n    const int bufmax = 3;\n    Option buffer[bufmax];\n    Option options[stats.options_max];\n    assert(!options[ABBREVIATE]);\n    Parser parse(multi_usage, count(multi3), multi3, options, buffer, 4, true, bufmax);\n    assert(!parse.error());\n    assert(parse.optionsCount() == bufmax);\n    assert(parse.nonOptionsCount() == 2);\n    assert(eq(parse.nonOptions()[0],\"nonoption1\"));\n    assert(eq(parse.nonOptions()[1],\"nonoption2\"));\n    assert(options[ABBREVIATE]);\n    assert(options[UNKNOWN].count() == 2); // because of buxmax the 2nd 'b' cannot be stored\n    assert(options[UNKNOWN].first()->name[0] == 'a' && options[UNKNOWN].first()->namelen == 1);\n    assert(options[UNKNOWN].first()->arg == 0);\n    assert(eq(options[UNKNOWN].last()->name,\"bb\"));\n  }\n  {\n    Stats stats(true, multi_usage, -1, reorder);\n    Option buffer[stats.buffer_max];\n    Option options[stats.options_max];\n    Parser parse(true, multi_usage, -1, reorder, options, buffer);\n    assert(!parse.error());\n    assert(parse.optionsCount() == 3);\n    assert(parse.nonOptionsCount() == 4);\n    assert(parse.nonOptions() == &reorder[6]);\n  }\n  {\n    Option buffer[10];\n    Option options[10];\n    Parser parse(true, multi_usage, 666, reorder2, options, buffer, 0, false, 10);\n    assert(!parse.error());\n    assert(parse.optionsCount() == 2);\n    assert(parse.nonOptionsCount() == 2);\n  }\n\n  fprintf(stdout, \"All tests passed.\\n\");\n  return 0;\n}\n\n"
  },
  {
    "path": "third-party/optionparser/testprintusage.cpp",
    "content": "/* Written 2012 by Matthias S. Benkmann\n *\n * The author hereby waives all copyright and related rights to the contents\n * of this example file (testprintusage.cpp) to the extent possible under the law.\n */\n\n/**\n * @file\n * @brief Test program for the option::printUsage() function.\n *\n * @note\n * This program is for developing TLMC++OP. It is neither an example nor a functionality test.\n * Do not worry if it doesn't compile or run on your platform.\n */\n\n#include <cstdio>\n#include <iostream>\n#include <sstream>\n#include <unistd.h>\n\n#include \"optionparser.h\"\n\nusing namespace std;\nusing option::Descriptor;\nusing option::Arg;\n\nconst Descriptor test_vtabs[] = {\n    {0,0,\"\",\"\",Arg::None, \"Cölüümn 1 line ı\\vColumn 1 line 2\\vColumn 1 line 3  \\t\\vColumn 2 line 2  \\tColumn 3 line 1\\v\\vColumn 3 line 3  \"},\n    {0,0,0,0,0,0}\n};\n\nconst Descriptor test_columns[] = {\n    {0,0,\"\",\"\",Arg::None, \"Column 1 line 1  \\t\\tColumn 3 line 1\\n\"\n                          \"Column 1 line 2  \\tColumn 2 line 2   \\tColumn 3 line 2\\n\"\n                          \"Column 1 line 3  \\t\\tColumn 3 line 3\" },\n    {0,0,0,0,0,0}\n};\n\nconst Descriptor test_column1[] = {\n    {0,0,\"\",\"\",Arg::None, \"11 \\t21\\v22\\v23\\t 31\\nxx\" },\n    {0,0,0,0,0,0}\n};\n\n\nconst Descriptor test_tables[] = {\n    {0,0,\"\",\"\",Arg::None,0}, // table break\n    {0,0,\"\",\"\",Arg::None,0}, // table break\n    {0,0,\"\",\"\",Arg::None, \"Each table has its own column widths and is not aligned with other tables.\"},\n    {0,0,\"\",\"\",Arg::None, \"Table 1 Column 1 Line 1 \\tTable 1 Column 2 Line 1 \\tTable 1 Column 3 Line 1\\n\"\n                          \"Table 1 Col 1 Line 2 \\tTable 1 Col 2 Line 2 \\tTable 1 Col 3 Line 2\"\n                           },\n    {0,0,\"\",\"\",Arg::None, \"Table 1 Col 1 Line 3 \\tTable 1 Col 2 Line 3 \\tTable 1 Column 3 Line 3\\n\"\n                          \"Table 1 Col 1 Line 4 \\tTable 1 Column 2 Line 4 \\tTable 1 Column 3 Line 4\"\n                           },\n    {0,0,\"\",\"\",Arg::None,0}, // table break\n    {0,0,\"\",\"\",Arg::None,0}, // table break\n    {0,0,\"\",\"\",Arg::None,  \"This is the only line of table 2.\" },\n    {0,0,\"\",\"\",Arg::None,0}, // table break\n    {0,0,\"\",\"\",Arg::None,  \"This is the very long 1st line of table 3. It is more than 80 characters in length and therefore needs to be wrapped. In fact it is so long that it needs to be wrapped multiple times to fit into a normal 80 characters terminal.\\v\"\n                           \"This is the very long 2nd line of table 3. It is more than 80 characters in length and therefore needs to be wrapped. In fact it is so long that it needs to be wrapped multiple times to fit into a normal 80 characters terminal.\\v\"\n                           \"This is a reasonably sized line 3 of table 3.\"\n                          },\n    {0,0,\"\",\"\",Arg::None,0}, // table break\n    {0,0,\"\",\"\",Arg::None, \"Table 4:\\n\"\n                          \"  \\tTable 4 C 1 L 1 \\tTable 4 C 2 L 1 \\tTable 4 C 3 L 1\\n\"\n                            \"\\tTable 4 C 1 L 2 \\tTable 4 C 2 L 2 \\tTable 4 C 3 L 2\"\n                           },\n    {0,0,\"\",\"\",Arg::None,0}, // table break\n    {0,0,\"\",\"\",Arg::None, \"This is the only line of table 5\"},\n    {0,0,\"\",\"\",Arg::None,0}, // table break\n    {0,0,\"\",\"\",Arg::None, \"Table 6 C 1 L 1 \\tTable 6 C 2 L 1 \\tTable 6 C 3 L 1\\n\"\n                          \"Table 6 C 1 L 2 \\tTable 6 C 2 L 2 \\tTable 6 C 3 L 2\"\n                          },\n    {0,0,\"\",\"\",Arg::None,0 }, // table break\n    {0,0,\"\",\"\",Arg::None, \"Table 7 Column 1 Line 1 \\tTable 7 Column 2 Line 1 \\tTable 7 Column 3 Line 1\\n\"\n                          \"Table 7 Column 1 Line 2 \\tTable 7 Column 2 Line 2 \\tTable 7 Column 3 Line 2\\n\"\n                          },\n    {0,0,0,0,0,0}\n};\n\nconst Descriptor test_nohelp[] = {\n    {0,0,\"\",\"\",Arg::None, 0 },\n    {0,0,\"\",\"\",Arg::None, 0 },\n    {0,0,\"\",\"\",Arg::None, 0 },\n    {0,0,0,0,0,0}\n};\n\nconst Descriptor test_wide[] = {\n    {0,0,\"\",\"\",Arg::None, \"Roma\\t|x漢\" },\n    {0,0,\"\",\"\",Arg::None, \"ｶﾀｶﾅ\\t|漢字\" },\n    {0,0,\"\",\"\",Arg::None, \"漢字\\t|漢ｶ \" },\n    {0,0,\"\",\"\",Arg::None, \"漢字\\t|ｶﾅ 漢字\" },\n    {0,0,0,0,0,0}\n};\n\nconst Descriptor test_overlong[] = {\n    {0,0,\"\",\"\",Arg::None, \"Good \\t| Good \\t| This is good.\" },\n    {0,0,\"\",\"\",Arg::None, \"Good \\t| This is an overlong cell. \\t| This is good.\" },\n    {0,0,\"\",\"\",Arg::None, \"Good \\t| Good \\t| This is good.\" },\n    {0,0,0,0,0,0}\n};\n\nconst Descriptor test_toomanycolumns[] = {\n    {0,0,\"\",\"\",Arg::None, \"This \\ttable \\thas \\ttoo \\tmany \\tcolumns. \\tThe \\tlast \\tcolumns \\tare \\tdiscarded.\" },\n    {0,0,\"\",\"\",Arg::None, \"1\\t2\\t3\\t4\\t5\\t6\\t7\\t8\\t9\\t10\\t11\" },\n    {0,0,0,0,0,0}\n};\n\nconst Descriptor test_ownline[] = {\n    {0,0,\"\",\"\",Arg::None, \"1234567890AB\\vBA0987654321\\tStarts on its own line and is indented somewhat.\\vThis one, too.\" },\n    {0,0,0,0,0,0}\n};\n\nconst Descriptor test_overflow[] = {\n    {0,0,\"\",\"\",Arg::None, \"漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字漢字\" },\n    {0,0,0,0,0,0}\n};\n\nvoid stderr_write(const char* str, int size)\n{\n   fwrite(str, size, 1, stderr);\n}\n\nstruct stderr_writer\n{\n  void write(const char* buf, size_t size) const\n  {\n    ::write(2, buf, size);\n  }\n};\n\nstruct stderr_write_functor\n{\n  void operator()(const char* buf, size_t size)\n  {\n    ::write(2, buf, size);\n  }\n};\n\nint main()\n{\n  fputs(\"---------------------------------------------------------------\\n\",stderr);\n  option::printUsage(stderr_write, test_overflow, 1);\n  fputs(\"---------------------------------------------------------------\\n\",stderr);\n  option::printUsage(stderr_write, test_vtabs);\n  fputs(\"---------------------------------------------------------------\\n\",stderr);\n  option::printUsage(stderr_writer(), test_columns);\n  fputs(\"---------------------------------------------------------------\\n\",stderr);\n  option::printUsage(write, 2, test_column1);\n  fputs(\"---------------------------------------------------------------\\n\",stderr);\n  option::printUsage(cerr, test_tables);\n  fputs(\"---------------------------------------------------------------\\n\",stderr);\n  option::printUsage(fwrite, stderr, test_nohelp);\n  fputs(\"---------------------------------------------------------------\\n\",stderr);\n  ostringstream sst;\n  option::printUsage(sst, test_wide, 8);\n  cerr<<sst.str();\n  fputs(\"---------------------------------------------------------------\\n\",stderr);\n  stderr_write_functor stderr_write_f;\n  option::printUsage(&stderr_write_f, test_overlong, 30);\n  fputs(\"---------------------------------------------------------------\\n\",stderr);\n  option::printUsage(stderr_write, test_toomanycolumns);\n  fputs(\"---------------------------------------------------------------\\n\",stderr);\n  option::printUsage(stderr_write, test_ownline, 20);\n  fputs(\"---------------------------------------------------------------\\n\",stderr);\n  return 0;\n}\n"
  },
  {
    "path": "third-party/vst3sdk.windows.patch",
    "content": "∩╗┐diff --git a/third-party/vst3sdk/pluginterfaces/base/fplatform.h b/third-party/vst3sdk/pluginterfaces/base/fplatform.h\nindex 53ac5c0..822a8c8 100644\n--- a/third-party/vst3sdk/pluginterfaces/base/fplatform.h\n+++ b/third-party/vst3sdk/pluginterfaces/base/fplatform.h\n@@ -107,7 +107,7 @@\n \t\t#define SMTG_CPP11_STDLIBSUPPORT SMTG_CPP11\n \t\t#define SMTG_CPP14 (__cplusplus >= 201402L || ((_MSC_FULL_VER >= 190024210L) && (_MSVC_LANG >= 201402L)))\n \t\t#define SMTG_CPP17 (__cplusplus >= 201703L || ((_MSC_FULL_VER >= 190024210L) && (_MSVC_LANG >= 201703L)))\n-\t\t#define SMTG_CPP20 (__cplusplus >= 202002L)\n+\t\t#define SMTG_CPP20 (__cplusplus >= 202002L || ((_MSC_FULL_VER >= 190024210L) && (_MSVC_LANG >= 202002L)))\n \t\t#define SMTG_CPP23 (__cplusplus >= 202302L)\n \t\t#define SMTG_HAS_NOEXCEPT ((_MSC_FULL_VER >= 190023026L) || (SMTG_INTEL_CXX11_MODE && SMTG_INTEL_COMPILER >= 1300))\n \t\t#if ((_MSC_FULL_VER >= 190024210L) || (SMTG_INTEL_CXX11_MODE && SMTG_INTEL_COMPILER >= 1500) || (defined (__MINGW32__) && SMTG_CPP11))\n"
  },
  {
    "path": "triplets/win-custom-x86-64.cmake",
    "content": "# Custom triplet for using with vcpkg, specific to this project.\n\nset(VCPKG_TARGET_ARCHITECTURE x64)\nset(VCPKG_CRT_LINKAGE dynamic)\n\nif (${PORT} MATCHES \"libsndfile\")\n    set(VCPKG_LIBRARY_LINKAGE dynamic)\nelse ()\n    set(VCPKG_LIBRARY_LINKAGE static)\nendif()"
  },
  {
    "path": "vcpkg.json",
    "content": "{\n  \"name\": \"sushi\",\n  \"version-string\": \"1.0.0\",\n  \"homepage\": \"https://github.com/elk-audio/sushi\",\n  \"description\": \"Headless plugin host for Elk Audio OS\",\n  \"dependencies\": [\n    {\n      \"name\": \"grpc\",\n      \"features\": [\n        \"codegen\"\n      ]\n    },\n    {\n      \"name\": \"libsndfile\",\n      \"default-features\": false\n    },\n    \"rtmidi\",\n    \"lv2\",\n    \"lilv\",\n    \"sentry-native\",\n    { \"name\": \"pkgconf\", \"platform\": \"windows\" }\n  ]\n}\n"
  }
]