Repository: baconpaul/six-sines Branch: main Commit: 89cf6cddcd5f Files: 271 Total size: 15.4 MB Directory structure: gitextract_s8got7r0/ ├── .clang-format ├── .github/ │ └── workflows/ │ └── build-plugin.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE.md ├── LIN_STANDALONE.md ├── MAC_STANDALONE.md ├── README.md ├── WIN_STANDALONE.md ├── cmake/ │ ├── CmakeRC.cmake │ ├── basic_installer_clapfirst.cmake │ └── compile-options.cmake ├── doc/ │ ├── 12_prompts/ │ │ ├── audio_in_prompt.md │ │ └── color_picker.md │ ├── 12_roadmap.md │ ├── ack.md │ ├── changelog.md │ └── manual.md ├── libs/ │ ├── CMakeLists.txt │ └── catch2/ │ └── include/ │ └── catch2/ │ └── catch2.hpp ├── resources/ │ ├── LICENSE_GPL3 │ ├── NightlyBlurb.md │ ├── ReadmeZip.txt │ ├── factory_patches/ │ │ ├── Bass/ │ │ │ ├── ...And Jack Had A Groove!.sxsnp │ │ │ ├── A Supporting Role.sxsnp │ │ │ ├── Bass 1.sxsnp │ │ │ ├── Bass 2.sxsnp │ │ │ ├── Bass 3.sxsnp │ │ │ ├── Bass 4.sxsnp │ │ │ ├── Bass Is The Place.sxsnp │ │ │ ├── Better Lately Than Never.sxsnp │ │ │ ├── Brand New Bass.sxsnp │ │ │ ├── Butterbean.sxsnp │ │ │ ├── Charlie Blackwood.sxsnp │ │ │ ├── Deep And Low.sxsnp │ │ │ ├── Deep Fried Low End.sxsnp │ │ │ ├── Glide Bass.sxsnp │ │ │ ├── Gnorch.sxsnp │ │ │ ├── Growler.sxsnp │ │ │ ├── I Am Sweepy.sxsnp │ │ │ ├── Laughing Bass.sxsnp │ │ │ ├── Lowkey Slap.sxsnp │ │ │ ├── Neon New York.sxsnp │ │ │ ├── Polite Discourse.sxsnp │ │ │ ├── Post-Acid Bass.sxsnp │ │ │ ├── Power Through It.sxsnp │ │ │ ├── Rehab For Edgelords.sxsnp │ │ │ ├── Scream Queen.sxsnp │ │ │ ├── Silversmith.sxsnp │ │ │ ├── Soft Bass.sxsnp │ │ │ ├── Soft Landing.sxsnp │ │ │ ├── Squeaky Clean.sxsnp │ │ │ ├── Sub Bass.sxsnp │ │ │ ├── Substrata.sxsnp │ │ │ ├── The Zone of Fat.sxsnp │ │ │ ├── This is not an 8-bit Bass.sxsnp │ │ │ ├── Warrior Macros.sxsnp │ │ │ └── You Got Nothin On This.sxsnp │ │ ├── Bells/ │ │ │ ├── Bellotar.sxsnp │ │ │ ├── Console Tones.sxsnp │ │ │ ├── Detuned Metal.sxsnp │ │ │ ├── ED2-10 Iso-2 Bells.sxsnp │ │ │ ├── ED2-11 Iso-2 Bells.sxsnp │ │ │ ├── ED2-12 Iso-2 Bells.sxsnp │ │ │ ├── ED2-13 Iso-2 Bells.sxsnp │ │ │ ├── ED2-23 Iso-2 Bells.sxsnp │ │ │ ├── ED2-5 Iso-2 Bells.sxsnp │ │ │ ├── ED2-6 Iso-2 Bells.sxsnp │ │ │ ├── ED2-7 Iso-2 Bells.sxsnp │ │ │ ├── ED2-8 Iso-2 Bells.sxsnp │ │ │ ├── ED2-9 Iso-2 Bells.sxsnp │ │ │ ├── Five Primes Bell.sxsnp │ │ │ ├── Inharmomen.sxsnp │ │ │ ├── Iso-Bells 5.sxsnp │ │ │ ├── Iso-Bells 6.sxsnp │ │ │ ├── Mirage Bell.sxsnp │ │ │ └── Odd-Bells 7.sxsnp │ │ ├── Drums/ │ │ │ ├── Hihat Closed.sxsnp │ │ │ ├── Hihat.sxsnp │ │ │ ├── Kick.sxsnp │ │ │ ├── Metal Percussion.sxsnp │ │ │ └── Snare Short.sxsnp │ │ ├── Effects/ │ │ │ ├── A Marching Chorus of Bees.sxsnp │ │ │ ├── Klaxon Feedback.sxsnp │ │ │ ├── Mothership.sxsnp │ │ │ ├── Noisines.sxsnp │ │ │ ├── Pulsator.sxsnp │ │ │ ├── Putting Out Fires.sxsnp │ │ │ ├── Rave Implosion.sxsnp │ │ │ ├── Reboot Sequence.sxsnp │ │ │ ├── Reverse Tech.sxsnp │ │ │ ├── Rhythm Blender.sxsnp │ │ │ ├── The Big Fuss II.sxsnp │ │ │ ├── The Big Fuss.sxsnp │ │ │ ├── Vacuum.sxsnp │ │ │ ├── Water Drop.sxsnp │ │ │ ├── Wind.sxsnp │ │ │ ├── You Are Breaking Up.sxsnp │ │ │ └── Zapper.sxsnp │ │ ├── Guitar/ │ │ │ └── Muted Strat.sxsnp │ │ ├── Keys/ │ │ │ ├── Afterschool Special.sxsnp │ │ │ ├── Arpeggiator Food.sxsnp │ │ │ ├── Bright and Synthy.sxsnp │ │ │ ├── EP 3.sxsnp │ │ │ ├── EP 4.sxsnp │ │ │ ├── EP to Organ.sxsnp │ │ │ ├── EPianotar.sxsnp │ │ │ ├── Eat Your Greens.sxsnp │ │ │ ├── Fuzzbox.sxsnp │ │ │ ├── Hello Supersaw.sxsnp │ │ │ ├── Iconoclasm.sxsnp │ │ │ ├── Moving Glass Organ.sxsnp │ │ │ ├── Nostalgic Bell Synth.sxsnp │ │ │ ├── Organ.sxsnp │ │ │ ├── Out With The New.sxsnp │ │ │ ├── Retrofuture Brass.sxsnp │ │ │ ├── Sow Some Seeds.sxsnp │ │ │ ├── Stack Operator.sxsnp │ │ │ ├── Surge DX Piano Port.sxsnp │ │ │ └── UpDown Funker.sxsnp │ │ ├── Leads/ │ │ │ ├── Active Radar.sxsnp │ │ │ ├── Airlock.sxsnp │ │ │ ├── Asteroid Destroyed.sxsnp │ │ │ ├── Memento Vivere.sxsnp │ │ │ ├── Plucky.sxsnp │ │ │ ├── Prog Jazzer.sxsnp │ │ │ ├── Rounded PWM.sxsnp │ │ │ ├── Rounded Sync.sxsnp │ │ │ ├── Rounded Sync2.sxsnp │ │ │ ├── Rounded Sync3.sxsnp │ │ │ ├── Sorta Super Saw.sxsnp │ │ │ ├── Teknophilia.sxsnp │ │ │ └── Whissle.sxsnp │ │ ├── MPE/ │ │ │ ├── Exp Bowed Glass.sxsnp │ │ │ ├── Exp Bowing 1.sxsnp │ │ │ ├── Exp Bowing 2.sxsnp │ │ │ ├── Exp Reeds.sxsnp │ │ │ └── Exp Slow Primes.sxsnp │ │ ├── Mallets/ │ │ │ ├── Bubblegum Marimba.sxsnp │ │ │ ├── CPS Bells.sxsnp │ │ │ └── Marimbell.sxsnp │ │ ├── Pads/ │ │ │ ├── Bag of Bells in the Next Room.sxsnp │ │ │ ├── Beauty Grit.sxsnp │ │ │ ├── Borgus.sxsnp │ │ │ ├── Conscious Vibrations.sxsnp │ │ │ ├── Crystal Blue.sxsnp │ │ │ ├── Daughter of FloatChime.sxsnp │ │ │ ├── Death From Below.sxsnp │ │ │ ├── Furbaby.sxsnp │ │ │ ├── Holding Pattern.sxsnp │ │ │ ├── Hope Evolution.sxsnp │ │ │ ├── OST Pad.sxsnp │ │ │ ├── Pictures of Nebulas.sxsnp │ │ │ ├── Subtle Starlight.sxsnp │ │ │ ├── The Sixth Element.sxsnp │ │ │ └── We are Not Alone.sxsnp │ │ ├── Percussion/ │ │ │ ├── Chroma Perc.sxsnp │ │ │ ├── High Hats of Glory.sxsnp │ │ │ └── Robot Companion.sxsnp │ │ ├── Strings/ │ │ │ ├── Bowed Glass.sxsnp │ │ │ ├── Bowing 1.sxsnp │ │ │ ├── Bowing 2.sxsnp │ │ │ ├── Familiar Strings.sxsnp │ │ │ └── Slow Primes.sxsnp │ │ └── Templates/ │ │ ├── INIT EP.sxsnp │ │ └── INIT Sine.sxsnp │ ├── factory_themes/ │ │ ├── Dark.sixtheme │ │ └── Light.sixtheme │ ├── fonts/ │ │ ├── Anonymous_Pro/ │ │ │ └── OFL.txt │ │ ├── Manrope/ │ │ │ ├── OFL.txt │ │ │ └── README.txt │ │ └── README.txt │ └── installer_mac/ │ ├── Icon.icns │ ├── License.txt │ ├── SideQuestIcon.icns │ ├── entitlements.plist │ ├── icns.rsrc │ └── make_rsrc.sh ├── scripts/ │ ├── denew.pl │ ├── fix_code.sh │ ├── fix_file_comments.pl │ ├── fix_header_guards.pl │ ├── fix_transpose.pl │ └── make_icon.py ├── src/ │ ├── clap/ │ │ ├── preset-discovery-impl.cpp │ │ ├── preset-discovery-impl.h │ │ ├── six-sines-clap-entry-impl.cpp │ │ ├── six-sines-clap-entry-impl.h │ │ ├── six-sines-clap-entry.cpp │ │ └── six-sines-clap.cpp │ ├── configuration.h │ ├── dsp/ │ │ ├── macro_node.h │ │ ├── matrix_node.h │ │ ├── node_support.h │ │ ├── op_source.h │ │ ├── remap_functions.h │ │ ├── resonant_window.h │ │ ├── sintable.cpp │ │ └── sintable.h │ ├── infra/ │ │ └── RIFFWavWriter.h │ ├── presets/ │ │ ├── preset-manager.cpp │ │ ├── preset-manager.h │ │ ├── ui-theme-manager.cpp │ │ └── ui-theme-manager.h │ ├── synth/ │ │ ├── macro_usage.cpp │ │ ├── macro_usage.h │ │ ├── matrix_index.h │ │ ├── mod_matrix.cpp │ │ ├── mod_matrix.h │ │ ├── mono_values.h │ │ ├── patch.cpp │ │ ├── patch.h │ │ ├── synth.cpp │ │ ├── synth.h │ │ ├── voice.cpp │ │ ├── voice.h │ │ └── voice_values.h │ └── ui/ │ ├── clipboard.cpp │ ├── clipboard.h │ ├── dahdsr-components.h │ ├── finetune-sub-panel.cpp │ ├── finetune-sub-panel.h │ ├── knob-highlight.h │ ├── lfo-components.h │ ├── macro-panel.cpp │ ├── macro-panel.h │ ├── macro-sub-panel.cpp │ ├── macro-sub-panel.h │ ├── main-panel.cpp │ ├── main-panel.h │ ├── main-sub-panel.cpp │ ├── main-sub-panel.h │ ├── mainpan-sub-panel.cpp │ ├── mainpan-sub-panel.h │ ├── matrix-panel.cpp │ ├── matrix-panel.h │ ├── matrix-sub-panel.cpp │ ├── matrix-sub-panel.h │ ├── mixer-panel.cpp │ ├── mixer-panel.h │ ├── mixer-sub-panel.cpp │ ├── mixer-sub-panel.h │ ├── modulation-components.h │ ├── patch-data-bindings.h │ ├── playmode-sub-panel.cpp │ ├── playmode-sub-panel.h │ ├── preset-data-binding.h │ ├── segmented-ratio-editor.cpp │ ├── segmented-ratio-editor.h │ ├── self-sub-panel.cpp │ ├── self-sub-panel.h │ ├── settings-panel.cpp │ ├── settings-panel.h │ ├── six-sines-editor-impl.h │ ├── six-sines-editor.cpp │ ├── six-sines-editor.h │ ├── six-sines-skin.h │ ├── source-panel.cpp │ ├── source-panel.h │ ├── source-sub-panel.cpp │ ├── source-sub-panel.h │ ├── spectrum-analyzer.cpp │ ├── spectrum-analyzer.h │ ├── ui-constants.h │ ├── ui-defaults.h │ └── waveform-display.h └── tests/ ├── CMakeLists.txt ├── factory_patches.cpp ├── structure.cpp └── test_main.cpp ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ --- BasedOnStyle: LLVM IndentWidth: 4 --- Language: Cpp BasedOnStyle: LLVM IndentWidth: 4 AlignAfterOpenBracket: Align BreakBeforeBraces: Allman ColumnLimit: 100 SortIncludes: false --- Language: ObjC BasedOnStyle: LLVM IndentWidth: 4 AlignAfterOpenBracket: Align BreakBeforeBraces: Allman ColumnLimit: 100 SortIncludes: false --- ================================================ FILE: .github/workflows/build-plugin.yml ================================================ name: Build Six Sines on: push: branches: - main - rel_* - test/* tags: - 'v**' pull_request: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TARGET_NAME: six-sines PLUGIN_NAME: "Six Sines" defaults: run: shell: bash jobs: build_plugin: name: Build - ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: matrix: include: - os: windows-latest name: windows-x64 cmake_args: -DCMAKE_CXX_COMPLIER=clang++ -DCMAKE_C_COMPILER=clang -GNinja - os: macos-latest name: macos cmake_args: -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -GNinja - os: ubuntu-latest name: linux-x64 cmake_args: -GNinja steps: - name: Checkout code uses: actions/checkout@v6 with: submodules: recursive - name: Prepare for JUCE uses: surge-synthesizer/sst-githubactions/prepare-for-juce@main with: os: ${{ runner.os }} - name: Install Inno Setup uses: surge-synthesizer/sst-githubactions/install-innosetup@main with: version: "6.7.1" - uses: apple-actions/import-codesign-certs@v3 if: runner.os == 'macOS' && github.event_name != 'pull_request' with: p12-file-base64: ${{ secrets.MAC_CERTS_P12 }} p12-password: ${{ secrets.CERT_PWD }} - name: Build pull request version if: github.event_name == 'pull_request' run: | cmake -S . -B ./build ${{ matrix.cmake_args }} -DCMAKE_BUILD_TYPE=Debug -DCOPY_AFTER_BUILD=FALSE -DGITHUB_ACTIONS_BUILD=TRUE cmake --build ./build --config Debug --target ${TARGET_NAME}-installer --parallel 3 - name: Build and run tests if: github.event_name == 'pull_request' run: | cmake --build ./build --config Debug --target six-sines-test --parallel 3 ./build/tests/six-sines-test - name: Build release version if: github.event_name != 'pull_request' run: | export MAC_SIGNING_CERT="${{ secrets.MAC_SIGNING_CERT_NAME }}" export MAC_INSTALLING_CERT="${{ secrets.MAC_INSTALLING_CERT_NAME }}" export MAC_SIGNING_ID="${{ secrets.MAC_SIGNING_ID }}" export MAC_SIGNING_1UPW="${{ secrets.MAC_SIGNING_1UPW }}" export MAC_SIGNING_TEAM="${{ secrets.MAC_SIGNING_TEAM }}" cmake -S . -B ./build ${{ matrix.cmake_args }} -DCMAKE_BUILD_TYPE=Release -DCOPY_AFTER_BUILD=FALSE -DGITHUB_ACTIONS_BUILD=TRUE cmake --build ./build --config Release --target ${TARGET_NAME}-installer --parallel 3 - name: Show Installer Directory run: | ls -l build/installer - name: Upload artifact if: github.event_name != 'pull_request' && runner.os != 'Linux' # We get this through docker uses: actions/upload-artifact@v4 with: path: build/installer name: dawplugin-${{ matrix.name }} build_plugin_docker: name: Build - Docker Ubuntu 22 runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 with: submodules: recursive - name: Build in Docker uses: surge-synthesizer/sst-githubactions/cmake-in-docker@main with: image: ghcr.io/surge-synthesizer/sst-dockerimages/ubuntu22_gcc12:main username: ${{ github.actor }} token: ${{ secrets.GITHUB_TOKEN }} cmakeArgs: -DCMAKE_BUILD_TYPE=Release -GNinja -DCOPY_AFTER_BUILD=FALSE -DGITHUB_ACTIONS_BUILD=TRUE target: six-sines-installer - name: Show Installer Directory run: | ls -l ./build/installer - name: Upload artifact if: github.event_name != 'pull_request' uses: actions/upload-artifact@v4 with: path: build/installer name: build-docker-linux publish-plugin-nightly: name: Publish Nightly if: ${{ github.ref == 'refs/heads/main' && github.repository_owner == 'baconpaul' }} runs-on: ubuntu-latest needs: [build_plugin, build_plugin_docker] steps: - name: Upload to Nightly uses: surge-synthesizer/sst-githubactions/upload-to-release@main with: tag: Nightly reuse_tag: true create_tag: false token: ${{ secrets.GITHUB_TOKEN }} release_blurb_file: resources/NightlyBlurb.md - name: Post to Discord uses: surge-synthesizer/sst-githubactions/discord-release-notify@main with: webhook: ${{ secrets.DISCORD_WEBHOOK }} tag: Nightly title: "A New Six Sines Nightly is Available" subtitle: "Have fun!" publish-plugin-release: name: Publish Release if: startsWith(github.ref, 'refs/tags/v') && github.repository_owner == 'baconpaul' runs-on: ubuntu-latest needs: [build_plugin, build_plugin_docker] steps: - name: Upload to Release uses: surge-synthesizer/sst-githubactions/upload-to-release@main with: tag: ${{ github.ref_name }} reuse_tag: false create_tag: true token: ${{ secrets.GITHUB_TOKEN }} - name: Post to Discord uses: surge-synthesizer/sst-githubactions/discord-release-notify@main with: webhook: ${{ secrets.DISCORD_WEBHOOK }} tag: ${{ github.ref_name }} title: "A New Six Sines Release is Available" subtitle: "But really, what does stable mean?" ================================================ FILE: .gitignore ================================================ build/ .DS_Store .idea/ .vs/ cmake-*/ ignore/ minst*.sh BUILD_VERSION CLAUDE.md ================================================ FILE: .gitmodules ================================================ [submodule "libs/sst/sst-basic-blocks"] path = libs/sst/sst-basic-blocks url = https://github.com/surge-synthesizer/sst-basic-blocks.git [submodule "libs/simde"] path = libs/simde url = https://github.com/simd-everywhere/simde [submodule "libs/clap-libs/clap"] path = libs/clap-libs/clap url = https://github.com/free-audio/clap.git [submodule "libs/clap-libs/clap-helpers"] path = libs/clap-libs/clap-helpers url = https://github.com/free-audio/clap-helpers.git [submodule "libs/clap-libs/clap-wrapper"] path = libs/clap-libs/clap-wrapper url = https://github.com/free-audio/clap-wrapper.git [submodule "libs/sst/sst-clap-helpers"] path = libs/sst/sst-clap-helpers url = https://github.com/surge-synthesizer/sst-clap-helpers.git [submodule "libs/JUCE"] path = libs/JUCE url = https://github.com/juce-framework/JUCE.git [submodule "libs/sst/sst-jucegui"] path = libs/sst/sst-jucegui url = https://github.com/surge-synthesizer/sst-jucegui.git [submodule "libs/sst/sst-voicemanager"] path = libs/sst/sst-voicemanager url = https://github.com/surge-synthesizer/sst-voicemanager.git [submodule "libs/MTS-ESP"] path = libs/MTS-ESP url = https://github.com/ODDSound/MTS-ESP [submodule "libs/fmt"] path = libs/fmt url = https://github.com/fmtlib/fmt.git [submodule "libs/sst/sst-cpputils"] path = libs/sst/sst-cpputils url = https://github.com/surge-synthesizer/sst-cpputils [submodule "libs/sst/sst-plugininfra"] path = libs/sst/sst-plugininfra url = https://github.com/surge-synthesizer/sst-plugininfra [submodule "libs/libsamplerate"] path = libs/libsamplerate url = https://github.com/libsndfile/libsamplerate.git [submodule "libs/sst/sst-cmake"] path = libs/sst/sst-cmake url = https://github.com/surge-synthesizer/sst-cmake ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.28) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15 CACHE STRING "Build for 10.14") set(CMAKE_POSITION_INDEPENDENT_CODE ON) project(six-sines VERSION 1.2.0 LANGUAGES C CXX) set(PRODUCT_NAME "Six Sines") option(USE_SANITIZER "Build and link with ASAN" FALSE) option(COPY_AFTER_BUILD "Will copy after build" TRUE) option(BUILD_SINGLE_ONLY "Only build the one plugin - no seven sines out" FALSE) include(cmake/compile-options.cmake) ## New version include(libs/sst/sst-plugininfra/cmake/git-version-functions.cmake) version_from_versionfile_or_git() message(STATUS "DISPLAY_VERSION=${GIT_IMPLIED_DISPLAY_VERSION}; COMMIT_HASH=${GIT_COMMIT_HASH}; BRANCH=${GIT_BRANCH}") include(cmake/CmakeRC.cmake) file(GLOB_RECURSE PATCHES "resources/factory_patches/*.sxsnp") cmrc_add_resource_library(${PROJECT_NAME}-patches NAMESPACE sixsines_patches ${PATCHES}) file(GLOB THEMES "resources/factory_themes/*.sixtheme") cmrc_add_resource_library(${PROJECT_NAME}-themes NAMESPACE sixsines_themes ${THEMES}) file(GLOB FONTS "resources/fonts/Manrope/static/*.ttf" "resources/fonts/Anonymous_Pro/*.ttf") cmrc_add_resource_library(${PROJECT_NAME}-fonts NAMESPACE sixsines_fonts ${FONTS}) set(JUCE_PATH "${CMAKE_SOURCE_DIR}/libs/JUCE") add_subdirectory(libs) add_library(${PROJECT_NAME}-impl STATIC src/clap/six-sines-clap.cpp src/clap/six-sines-clap-entry-impl.cpp src/clap/preset-discovery-impl.cpp src/ui/six-sines-editor.cpp src/ui/spectrum-analyzer.cpp src/ui/clipboard.cpp src/ui/main-panel.cpp src/ui/main-sub-panel.cpp src/ui/matrix-panel.cpp src/ui/matrix-sub-panel.cpp src/ui/self-sub-panel.cpp src/ui/mixer-panel.cpp src/ui/mixer-sub-panel.cpp src/ui/source-panel.cpp src/ui/source-sub-panel.cpp src/ui/segmented-ratio-editor.cpp src/ui/macro-panel.cpp src/ui/macro-sub-panel.cpp src/ui/finetune-sub-panel.cpp src/ui/mainpan-sub-panel.cpp src/ui/playmode-sub-panel.cpp src/ui/settings-panel.cpp src/presets/preset-manager.cpp src/presets/ui-theme-manager.cpp src/dsp/sintable.cpp src/synth/synth.cpp src/synth/voice.cpp src/synth/patch.cpp src/synth/mod_matrix.cpp src/synth/macro_usage.cpp ) target_include_directories(${PROJECT_NAME}-impl PUBLIC src) target_compile_definitions(${PROJECT_NAME}-impl PRIVATE PRODUCT_NAME="${PRODUCT_NAME}" ) if (${BUILD_SINGLE_ONLY}) message(STATUS "Building single plugin version only") target_compile_definitions(${PROJECT_NAME}-impl PRIVATE CLAP_PLUGIN_COUNT=1 ) endif() if (WIN32) message(STATUS "Activating wchar presets") target_compile_definitions(${PROJECT_NAME}-impl PUBLIC USE_WCHAR_PRESET=1) endif() target_link_libraries(${PROJECT_NAME}-impl PUBLIC clap ) target_link_libraries(${PROJECT_NAME}-impl PUBLIC # PW change to public since we now have tests simde clap-helpers clap-wrapper-extensions mts-esp-client fmt-header-only sst-basic-blocks sst-voicemanager sst-jucegui sst-cpputils sst-plugininfra sst-plugininfra::filesystem sst-plugininfra::tinyxml sst-plugininfra::strnatcmp sst-plugininfra::patchbase sst-plugininfra::version_information sst::clap_juce_shim sst::clap_juce_shim_headers juce::juce_dsp ${PROJECT_NAME}-patches ${PROJECT_NAME}-themes ${PROJECT_NAME}-fonts samplerate ) target_compile_definitions(clap-wrapper-compile-options-public INTERFACE CLAP_WRAPPER_LOGLEVEL=0) make_clapfirst_plugins( TARGET_NAME ${PROJECT_NAME} IMPL_TARGET ${PROJECT_NAME}-impl OUTPUT_NAME "${PRODUCT_NAME}" ENTRY_SOURCE src/clap/six-sines-clap-entry.cpp BUNDLE_IDENTIFER "org.baconpaul.six-sines" BUNDLE_VERSION ${PROJECT_VERSION} COPY_AFTER_BUILD ${COPY_AFTER_BUILD} PLUGIN_FORMATS CLAP VST3 AUV2 ASSET_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${PROJECT_NAME}_assets WINDOWS_FOLDER_VST3 TRUE # You can add a target-per-standalone you want. Syntax here is # target-postfix output-name clap-id # This allows you to make multiple standalones from a multi-plugin clap STANDALONE_CONFIGURATIONS standalone "${PRODUCT_NAME}" "org.baconpaul.six-sines" STANDALONE_MACOS_ICON "${CMAKE_SOURCE_DIR}/resources/installer_mac/Icon.icns" ) include(cmake/basic_installer_clapfirst.cmake) add_clapfirst_installer( INSTALLER_TARGET ${PROJECT_NAME}-installer ASSET_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${PROJECT_NAME}_assets INSTALLER_PREFIX "six-sines" PRODUCT_NAME "${PRODUCT_NAME}" TARGETS ${PROJECT_NAME}_clap ${PROJECT_NAME}_vst3 ${PROJECT_NAME}_auv2 ${PROJECT_NAME}_standalone ) add_subdirectory(tests EXCLUDE_FROM_ALL) ================================================ FILE: LICENSE.md ================================================ Copyright 2019-2020, Paul Walker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: LIN_STANDALONE.md ================================================ # Linux Standalone: Code Review & Bug Analysis **Reviewed:** `libs/clap-libs/clap-wrapper/src/detail/standalone/linux/` **Files:** `x11_gui.h`, `x11_gui.cpp`, plus shared `standalone_host_audio.cpp`, `standalone_host_midi.cpp`, `entry.cpp` **Last updated:** 2026-04-04 **Context:** Ongoing work on the Linux standalone host for CLAP plugins. GTK path has been removed; X11 is the only GUI path. --- ## What Has Been Fixed | # | Fix | |---|-----| | 1 | `XOpenDisplay()` return value checked; exits with a message on failure | | 2 | `XInitThreads()` called before any Xlib call | | 3 | `MapNotify` handler guards `plugin` and `_ext._gui` before dereferencing | | 4 | GTK path removed entirely; X11 is unconditional on Linux | | 5 | MIDI port open failure now caught per-port; loop continues on error | | 6 | `adjust_size()` adjusted values are used for window creation | | 7 | `epoll_fd` set to -1 after close in `shutdown()` so the guard in `unregister_timer` fires correctly | | 8 | `gui->destroy()` called in `X11Gui::shutdown()` before epoll/display teardown, fixing timer unregistration crash on exit | --- ## X11 GUI: Remaining Issues ### Window Position Hard-Coded **File:** `x11_gui.cpp` — `XCreateSimpleWindow` call in `setPlugin()` Always opens at `(10, 10)`. No centering, no saved position restore. --- ### Window Size Locked via Size Hints **File:** `x11_gui.cpp` — `resetSizeTo()` ```cpp hints->min_width = hints->max_width = w; hints->min_height = hints->max_height = h; ``` Min and max hints are set to the same value on every resize. This prevents the window manager from allowing user-initiated resizing even for plugins that support `can_resize()`. To allow resizing, only `PSize` / `PAspect` hints should be set; min/max should be omitted or set to sensible bounds. --- ### `set_parent()` Only Called on `MapNotify` **File:** `x11_gui.cpp` — `runloop()` The plugin's `set_parent()` and `show()` are deferred until the window receives a `MapNotify` event. This is fine for most plugins but means the plugin has no parent window during the window-creation phase. Plugins that use `set_parent` to initialize their renderer may produce a blank frame on first display. --- ### No Wayland Support Neither XWayland-fallback nor native Wayland. On a Wayland-only session `XOpenDisplay` will fail (caught and exited cleanly now), but the user gets no hint about setting `DISPLAY` or `GDK_BACKEND=x11`. A more helpful exit message could suggest `DISPLAY=:0` or the XWayland socket. --- ### Epoll Error Handling: Failures Logged but Not Propagated `epoll_ctl` failures in `register_fd`, `register_timer`, `setPlugin` are logged and return false, but callers generally ignore the return value. A failed epoll registration means the corresponding timer or fd will never fire, silently breaking plugin functionality. --- ### Magic Numbers `nextTimerId` starts at `2112`, epoll max events is `256`, epoll timeout is `50ms`. None are named constants. --- ### No RAII for X11 Resources `XCloseDisplay`, `XFree`, timerfd closes, and epoll fd close are all manual. An exception or early return will leak resources. The timer fds in `timerIdToFd` are not closed in `shutdown()` if any are still registered. --- ## Audio Backend: ALSA / JACK / PipeWire ### The Core Problem **File:** `standalone_host_audio.cpp:60` ```cpp setAudioApi(RtAudio::Api::UNSPECIFIED); ``` `RtAudio::Api::UNSPECIFIED` lets RtAudio auto-select from whichever backends were compiled in. The selection priority in RtAudio is approximately JACK → ALSA → PulseAudio, but in practice **ALSA is almost always chosen** even when a JACK daemon is running, because RtAudio's auto-select logic does not reliably detect a running JACK server. This is the confirmed cause of the user-reported "only binds to ALSA, doesn't show JACK" symptom. The compiled-in backends are controlled by CMake options in `wrap_standalone.cmake` (inherited from RtAudio's own CMakeLists): ```cmake option(RTAUDIO_API_ALSA "Build ALSA API" ${LINUX}) option(RTAUDIO_API_PULSE "Build PulseAudio API" ${pulse_FOUND}) option(RTAUDIO_API_JACK "Build JACK audio server API" ${HAVE_JACK}) ``` JACK support is only compiled in when `HAVE_JACK` is true at configure time (i.e., JACK headers are present on the build machine). On a build machine without JACK headers the JACK backend is silently absent from the binary. ### PipeWire PipeWire is **not explicitly configured** — there is no `RTAUDIO_API_PIPEWIRE` option. PipeWire provides: - An **ALSA compatibility layer** (`pipewire-alsa`) — when active, ALSA device enumeration goes through PipeWire transparently. This usually works without any code changes. - A **JACK compatibility layer** (`pipewire-jack`) — replaces `libjack` with a PipeWire shim. If the build machine has `pipewire-jack` installed in place of real JACK, the JACK backend will talk to PipeWire's JACK server. This also generally works. - A **native PipeWire RtAudio backend** exists in newer RtAudio versions (`RtAudio::Api::LINUX_PIPEWIRE`). This is not enabled in the current build and would require adding `RTAUDIO_API_PIPEWIRE` to the CMake options. In short: on a system running PipeWire with the ALSA and JACK compatibility layers active, the existing code works but the user gets ALSA latency characteristics rather than PipeWire's low-latency graph scheduling. For most users this is acceptable. ### How to Address the JACK/API Selection Problem The `StandaloneHost` already has all the infrastructure needed: - `getCompiledApi()` — returns `std::vector` of built-in backends - `setAudioApi(RtAudio::Api)` — switches the active backend - `audioApi`, `audioApiName`, `audioApiDisplayName` — current state What is missing is a way to surface this to the user. The two practical options are: **Option A — Command-line flag (minimal, recommended first step)** Parse a `--audio-api ` argument in `wrapasstandalone.cpp` before calling `mainStartAudio()`. Map the name to an `RtAudio::Api` value and call `getStandaloneHost()->setAudioApi(...)`. Example mapping: | Flag value | `RtAudio::Api` | |------------|---------------| | `alsa` | `LINUX_ALSA` | | `jack` | `UNIX_JACK` | | `pulse` | `LINUX_PULSE` | | `pipewire` | `LINUX_PIPEWIRE` (if compiled) | Add `--list-audio-apis` to print the compiled-in options and exit. Use `getCompiledApi()` for the list. **Option B — Prefer JACK automatically when the server is reachable** Before defaulting to `UNSPECIFIED`, check whether JACK is compiled in and whether a JACK server is running: ```cpp // Pseudo-code auto apis = getCompiledApi(); if (std::find(apis.begin(), apis.end(), RtAudio::Api::UNIX_JACK) != apis.end()) { // Try JACK first; fall back to UNSPECIFIED if stream open fails setAudioApi(RtAudio::Api::UNIX_JACK); } ``` RtAudio will return an error when opening the stream if no JACK server is running, so the fallback path is safe. ### Device Enumeration and Selection - **No device selection UI or flag.** `getDefaultInputDevice()` / `getDefaultOutputDevice()` are used unconditionally. On systems where the default device is a USB headset or HDMI output this produces confusing behaviour. A `--audio-device ` flag (matched against `getDeviceNames()`) would address this. - **Default device IDs not validated.** If the default device ID returned is 0 and no device 0 exists for the selected API, `openStream` will fail with a potentially cryptic error. - **No input device selection.** `startAudioThread()` always passes the default input device. Plugins with audio inputs have no way to select their source. ### Audio Backend: Remaining Code Issues --- ### Channel Count Hard-Coded to 2 (Stereo Only) **File:** `standalone_host_audio.cpp:96, 101` ```cpp startAudioThreadOn(in, 2, ..., out, 2, ...); ``` Input and output are always opened as stereo. Plugins with mono or multi-channel buses will get the wrong channel count. `clapProcess` also asserts `inputChannelByBus[inp] == 2` and `outputChannelByBus[oup] == 2`. --- ### Buffer Size Defaults to 256, Not Device-Queried **File:** `standalone_host_audio.cpp:246-248` ```cpp if (currentBufferSize == 0) { currentBufferSize = 256; } ``` `currentBufferSize` can be set externally before `startAudioThread()` is called, but there is no UI or command-line path to do so. The default of 256 may be wrong for the selected device or API (JACK has its own period size; ALSA may not support 256 frames on all hardware). --- ### Xrun Detection Callback Is Empty **File:** `standalone_host_audio.cpp:24-26` ```cpp if (status) { } ``` RtAudio passes a non-zero `status` on xrun (underflow/overflow). The callback body is empty — xruns are silently dropped. `rtaErrorCallback` does catch `RTAUDIO_OUTPUT_UNDERFLOW` and `RTAUDIO_INPUT_OVERFLOW` separately, but only logs the first occurrence. --- ### `stopAudioThread()` Busy-Waits Up to 10 Seconds **File:** `standalone_host_audio.cpp:306-310` ```cpp for (auto i = 0; i < 10000 && !finishedRunning; ++i) { std::this_thread::sleep_for(1ms); } ``` Waits for the audio callback to set `finishedRunning`. If the stream stalls or the callback never fires again after `running = false`, this silently times out after 10 seconds and then calls `stopStream()` anyway. No log or error is emitted on timeout. --- ### `getSampleRates()` Has a Dead-Code Bug **File:** `standalone_host_audio.cpp:150-157` ```cpp auto samplesRates{rtaDac->getDeviceInfo(audioInputDeviceID).sampleRates}; // unused for (auto &sampleRate : rtaDac->getDeviceInfo(audioInputDeviceID).sampleRates) // queried again ``` The device info is fetched twice; the first result (`samplesRates`) is never read. --- ### `getBufferSizes()` Returns a Hard-Coded List **File:** `standalone_host_audio.cpp:164-166` ```cpp std::vector res{16, 32, 48, 64, 96, 128, 144, 160, 192, 224, 256, 480, 512, 1024, 2048, 4096}; ``` Not queried from the device. RtAudio doesn't expose valid buffer sizes for a device, so this is largely unavoidable — but the list includes non-power-of-two sizes (48, 96, 144, 160, 192, 224, 480) that many ALSA and JACK drivers will reject. --- ## MIDI: Remaining Issues ### Fatal Exit on MIDI Init Failure **File:** `standalone_host_midi.cpp:25-29` ```cpp catch (RtMidiError &error) { error.printMessage(); exit(EXIT_FAILURE); } ``` If creating the initial `RtMidiIn` (used only to count available ports) fails, the process exits hard. This kills the standalone even when MIDI is not needed. Should log a warning and continue with `numMidiPorts = 0`. --- ### `RtMidiIn` Created Twice Per Session **File:** `standalone_host_midi.cpp:22-23, 36` A temporary `RtMidiIn` is created just to call `getPortCount()`, then discarded. A separate instance is created for each port in the loop. The first instance is wasted. This can cause issues on some ALSA configurations where creating an `RtMidiIn` registers a client with the sequencer. --- ### All Ports Opened Unconditionally There is no mechanism to select which MIDI inputs to use. Every available port is opened. On systems with many virtual/loopback ALSA ports this can produce unwanted MIDI loops. --- ### 3-Byte Message Cap; No SysEx **File:** `standalone_host_midi.cpp:53` ```cpp if (nBytes <= 3) ``` Messages longer than 3 bytes are silently discarded. SysEx (which can be arbitrarily long) is never forwarded to the plugin. --- ### No Timestamp Preservation MIDI timestamps from the RtMidi callback (`deltatime`) are ignored. All events are queued with `header.time = 0`, meaning they are all presented to the plugin at the start of each audio block regardless of when they arrived. --- ## Summary Table | # | Item | Severity | Status | |---|------|----------|--------| | 1 | `XOpenDisplay()` not checked | Critical | Fixed | | 2 | No `XInitThreads()` | Critical | Fixed | | 3 | `MapNotify` null dereference | Critical | Fixed | | 4 | GTK `std::terminate()` on Wayland | High | Fixed (GTK removed) | | 5 | MIDI port open aborts loop | High | Fixed | | 6 | `adjust_size()` result discarded | Medium | Fixed | | 7 | `epoll_fd` not cleared to -1 after close | Medium | Fixed | | 8 | Timer unregistration crash on exit | Critical | Fixed | | — | `RtAudio::UNSPECIFIED` misses JACK; no API/device selection | High | Open | | — | Channel count hard-coded to stereo | High | Open | | — | Fatal exit if MIDI init fails | Medium | Open | | — | Buffer size defaults to 256, not configurable | Medium | Open | | — | Window size locked via min=max hints | Medium | Open | | — | `RtMidiIn` created twice | Low | Open | | — | All MIDI ports opened unconditionally | Low | Open | | — | No SysEx; 3-byte message cap | Low | Open | | — | No MIDI timestamp preservation | Low | Open | | — | Xrun callback body empty | Low | Open | | — | `stopAudioThread` 10s busy-wait, no timeout log | Low | Open | | — | `getSampleRates()` double-fetches device info | Low | Open | | — | Window position hard-coded to (10, 10) | Low | Open | | — | Leaks timer fds on shutdown if not unregistered | Low | Open | | — | No Wayland support or helpful error hint | Low | Open | ================================================ FILE: MAC_STANDALONE.md ================================================ # macOS Standalone: Code Review & Bug Analysis **Reviewed:** `libs/clap-libs/clap-wrapper/src/detail/standalone/macos/` **Files:** `AppDelegate.h`, `AppDelegate.mm`, `StandaloneFunctions.mm`, `Info.plist.in`, `MainMenu.xib`, plus shared `standalone_host_audio.cpp`, `standalone_host_midi.cpp`, `entry.cpp` **Date:** 2026-04-03 **Context:** Critical code review of the macOS standalone host for CLAP plugins. --- ## Architecture Summary The macOS standalone uses a conventional Cocoa application structure: 1. **Entry point** — `wrapasstandalone.mm` calls `NSApplicationMain()` 2. **XIB** — `MainMenu.xib` loads the window and wires `ClapWrapperAppDelegate` as the app delegate 3. **App delegate** — `AppDelegate.mm` drives all plugin, GUI, and audio setup from `applicationDidFinishLaunching:` via a 0.001 s one-shot timer 4. **Plugin/audio** — `entry.cpp` and shared `standalone_host_*.cpp` own the plugin instance and RtAudio/RtMidi streams --- ## Critical Bugs ### ~~Bug 1 — Retina Scale Factor Hardcoded to 1.0~~ (RETRACTED) **File:** `AppDelegate.mm` (inside `doSetup`) ```objc ui->set_scale(p, 1); // harmless; ignored per CLAP spec for Cocoa ``` This was originally flagged as critical, but the CLAP GUI spec explicitly states for `CLAP_WINDOW_API_COCOA`: > *uses logical size, don't call clap_plugin_gui->set_scale()* Cocoa plugins work in logical pixels and handle their own Retina scaling internally (via NSView backing scale APIs). The `set_scale()` call is specified to be ignorable — a conforming plugin returns `false` and does nothing. The `1.0` passed here is a harmless defensive call in case a non-conforming plugin needs a nudge, but it is not the cause of any rendering problem. **Retina rendering quality is entirely the plugin's responsibility under Cocoa.** The host's only obligation is to add `NSHighResolutionCapable` to the Info.plist (Bug 2), which opts the process into high-resolution mode so the window system does not pre-scale the backing store away from the plugin. --- ### Bug 2 — `NSHighResolutionCapable` Missing from Info.plist (CRITICAL — compounds Bug 1) **File:** `Info.plist.in` Without `NSHighResolutionCapable = true`, macOS opts the app into legacy low-resolution mode and will not pass a 2× backing scale through the window system at all. The plugin GUI renders in a blurry, scaled-up bitmap regardless of what `set_scale()` is told. Required addition to `Info.plist.in`: ```xml NSHighResolutionCapable ``` --- ### Bug 3 — Spinlock in the Audio Callback (HIGH — causes audio dropouts) **File:** `standalone_host.cpp` ```cpp ClapWrapper::detail::shared::SpinLockGuard processLockGuard(processLock); ``` The RtAudio callback acquires a spinlock on every audio block. If the main thread holds this lock while doing any GUI work, the audio thread busy-waits — consuming a full CPU core and causing glitches, dropouts, and system unresponsiveness. This is a well-known anti-pattern in real-time audio code. Spinlocks are never safe in an audio callback path. The correct approach is to use lock-free atomic flags or message queues to communicate state changes without blocking the audio thread. --- ### Bug 4 — No Error Checking After `ui->create()` (HIGH — silent invisible window) **File:** `AppDelegate.mm` (`doSetup`) ```objc if (!ui->is_api_supported(p, CLAP_WINDOW_API_COCOA, false)) LOGINFO("[WARNING] GUI API not supported"); ui->create(p, CLAP_WINDOW_API_COCOA, false); // return value ignored ui->set_scale(p, 1); // return value ignored ui->get_size(p, &w, &h); // if this returns 0,0 → invisible window ui->set_parent(p, &win); // return value ignored ui->show(p); // return value ignored ``` If `create()` fails, every subsequent call operates on undefined state. If `get_size()` returns `(0, 0)`, `setContentSize:` produces a zero-size window — visible in the Dock but showing nothing. None of these return values are checked, and no error is surfaced to the user. --- ### Bug 5 — MIDI Queue Missing Memory Fence (HIGH — data races on Apple Silicon) **File:** `standalone_host_midi.cpp` / shared `fixedqueue` The lock-free queue used to pass MIDI events from the RtMidi callback thread to the audio thread uses an atomic index but no explicit memory fence between the element write and the index update: ```cpp _elements[_head] = *val; // array write _head = (_head + 1) & _wrapMask; // atomic index update — but no fence before this ``` On Apple Silicon (ARM), the store-release guarantee on `_head` does not order the preceding array write without an explicit `std::atomic_thread_fence(std::memory_order_release)` between them. The audio thread can read a partially-written MIDI event. This can manifest as wrong note values, stuck notes, or silent corruption. --- ### Bug 6 — Memory Leak in AudioSettingsWindow (MEDIUM) **File:** `AppDelegate.mm` (Audio Settings handler) ```objc auto *window = [[AudioSettingsWindow alloc] initWithContentRect:windowRect styleMask:... backing:NSBackingStoreBuffered defer:NO]; ``` If ARC is not enabled (the Objective-C++ mix in this project makes it easy to inadvertently compile without ARC), this `alloc/init` window is never released. Each time the user opens the Audio Settings panel, a new `AudioSettingsWindow` is allocated and leaked. The same pattern is used for the `NSAlert` instances at multiple sites (plugin load failure, audio error dialogs). --- ## Setup Timing Issues ### The 0.001 s Deferred Setup **File:** `AppDelegate.mm` (`applicationDidFinishLaunching:`) ```objc [NSTimer scheduledTimerWithTimeInterval:0.001 target:self selector:@selector(doSetup) userInfo:nil repeats:NO]; ``` All plugin creation, GUI embedding, and audio initialization happen inside this 0.001 s timer callback. The intent is to let the run loop complete its first spin before doing work. In practice, 1 ms is not a meaningful guarantee — on a loaded system this fires at an arbitrary time. More significantly: - The window is shown (`orderFrontRegardless`) **before** the plugin GUI is embedded. If setup is slow the user briefly sees an empty window. - If setup fails partway through, the window remains visible with no content and no error. ### Run Loop Mode for the Callback Timer **File:** `AppDelegate.mm` ```objc [runLoop addTimer:self.requestCallbackTimer forMode:NSDefaultRunLoopMode]; ``` The timer that handles `on_main_thread` plugin callbacks is added to `NSDefaultRunLoopMode` only. During live window resizing or menu tracking, macOS switches the run loop to `NSEventTrackingRunLoopMode`. The callback timer does not fire in that mode, so plugin `on_main_thread` callbacks are silently delayed until the user stops interacting with the UI. Fix: use `NSRunLoopCommonModes` instead. --- ## Window Sizing ### Variable Shadowing in Resize Logic **File:** `AppDelegate.mm` (`windowWillResize:toSize:`) ```objc NSWindow *w = sender; // ← outer 'w' // ... uint32_t w, h; // ← inner 'w' shadows NSWindow *w plugin->_ext._gui->get_size(plugin->_plugin, &w, &h); ``` The `uint32_t w` shadows the `NSWindow *w` declared a few lines above. This does not cause a crash in the current code path because the outer `w` is not used after the shadow is introduced, but it is a maintenance hazard and suggests the resize logic was written hastily. ### No Bounds Checking on Plugin-Reported Size `get_size()` is called without validating that the returned width and height are sensible (non-zero, not absurdly large). A misbehaving plugin could return `(0, 0)` or `(65535, 65535)`, with no guard. --- ## Error Handling Multiple critical operations have no user-visible error path: | Site | Failure Mode | Current Handling | |------|--------------|-----------------| | Plugin entry not found | No plugin loaded | `LOGINFO` only, window shown empty | | `plugin->create()` fails | Null plugin pointer used | `LOGINFO` only | | `ui->create()` fails | GUI in undefined state | Not checked at all | | `ui->get_size()` returns (0,0) | Zero-size window | Not checked | | `mainStartAudio()` fails | No audio | Not checked; user hears nothing | | RtAudio stream open fails | Logged, stream closed | No dialog; audio silently absent | --- ## MIDI Notes - **`startMIDIThread()`** is a misnomer — it runs on the main thread. RtMidi callbacks fire on an internal RtMidi thread, not a thread this code controls. - **MIDI output** is explicitly noted as unimplemented: `LOGINFO("[WARNING] Midi Output not supported yet")` - **SysEx** is limited to 3 bytes across all platforms (shared code). - **No port selection**: all available MIDI input ports are opened unconditionally. --- ## Info.plist Issues | Key | Status | Impact | |-----|--------|--------| | `NSMicrophoneUsageDescription` | Present | Correct — needed for audio input permission | | `NSHighResolutionCapable` | **Missing** | Retina rendering broken (see Bug 2) | | `NSSupportsAutomaticGraphicsSwitching` | Missing | App may hold discrete GPU when not needed | --- ## Incomplete / TODO Items in Shared Code | Location | Note | |----------|------| | `standalone_host.cpp:160` | `// TODO cleaner` around main bus index logic | | `standalone_host.cpp:179` | Same TODO, duplicated | | `standalone_host_audio.cpp:244` | `"[WARNING] Hardcoding frame size to 256 samples for now"` — buffer size is not configurable | | `standalone_host_midi.cpp:103` | `"[WARNING] Midi Output not supported yet"` | | `AppDelegate.h:7` | Commented-out `AudioSettingsWindowDelegate` — unclear if still planned | --- ## Summary Table | # | Bug | Severity | Impact | |---|-----|----------|--------| | 1 | `set_scale()` call with Cocoa | ~~Critical~~ N/A | Retracted — Cocoa spec says don't call `set_scale()`; plugin ignores it per spec | | 2 | `NSHighResolutionCapable` missing from plist | Critical | macOS forces low-res mode; plugin has no chance to render at Retina resolution | | 3 | Spinlock in audio callback | High | Audio dropouts during any GUI interaction | | 4 | No error checking after `ui->create()` | High | Silent invisible window on any GUI failure — *(Done)* | | 5 | MIDI queue missing memory fence | High | Data races / corruption on Apple Silicon | | 6 | `AudioSettingsWindow` leaked each open | Medium | Growing memory use over session | | 7 | Callback timer on `NSDefaultRunLoopMode` only | Medium | `on_main_thread` callbacks drop during resize/menus — *(Done)* | | 8 | Zero-size window if `get_size()` returns (0,0) | Medium | Window exists but shows nothing — *(Done)* | | 9 | No user-visible audio init error | Medium | Silent audio failure | | 10 | Variable shadowing in resize handler | Low | Maintenance hazard, not a current crash | | 11 | 0.001 s deferred setup is fragile | Low | Window briefly blank; bad failure UX | | 12 | `NSSupportsAutomaticGraphicsSwitching` missing | Low | May keep discrete GPU alive | --- ## Recommended Fixes (Priority Order) 1. **Add `NSHighResolutionCapable = true`** to `Info.plist.in` immediately. *(Done)* 2. ~~**Fix `set_scale()`**~~ — retracted; `set_scale()` must not be called for `CLAP_WINDOW_API_COCOA` per the CLAP spec. The existing call at `1.0` is harmless but should not be changed to read `backingScaleFactor`. 3. **Remove the spinlock from the audio callback** — use atomic flags or a lock-free ring buffer for plugin ↔ audio-thread state. 4. **Add error checking** after `ui->create()`, `ui->set_parent()`, and `mainStartAudio()`, with user-facing `NSAlert` dialogs. *(Done)* 5. **Add a memory fence** in the MIDI queue between the element write and the index update. 6. **Switch the callback timer** to `NSRunLoopCommonModes`. *(Done)* 7. **Audit ARC coverage** — ensure `NSAlert` and `AudioSettingsWindow` instances are properly released. 8. **Validate sizes** returned by `ui->get_size()` before calling `setContentSize:`. *(Done)* 9. **Rename `startMIDIThread()`** and document which thread MIDI callbacks fire on. ================================================ FILE: README.md ================================================ # Six Sines Six Sines is a small synthesizer which explores audio rate inter-modulation of signals. Some folks would call it "an FM synth" but it's really a bit more PM/AM, and anyway that's kinda not the point. If you want to read a manual, [go here](doc/manual.md) If you want to download a release or recent version, [go here](https://github.com/baconpaul/six-sines/releases). We have versioned releases on that download page and additionally provide a [nightly](https://github.com/baconpaul/six-sines/releases/tag/Nightly) with the latest code at all times. In March 2025, we released version 1.1. You can read the changelog [here](doc/changelog.md) And please read the [acknowldgements](doc/ack.md) for a list of thanks. ## Hey are there any demos of this? Kinsey Dulcet, who designed many of the factory patches, has a demo track showing the more-80s-inspired sounds you can make. She's also working on a more 2020s inspired version, and that's pretty exciting! [Listen to Kinsey's track, 'Retrocade Nights'](https://soundcloud.com/kinseydulcet/retrocade-nights-six-sines-demo) The [One Synth Challenge](https://www.kvraudio.com/forum/viewtopic.php?t=618178) community used Six Sines as we were developing 1.1 to host OSC 192, and the results are amazing. You can hear all the entries here, made entirely with Six Sines and built in DAW effects. [Listen to the One Synth Challenge Playlist](https://soundcloud.com/kvrosc/sets/one-synth-challenge-192-six) ## Background The project exists for a few reasons 1. At the end of 2024, I was in a bit of a slump dev-wise for a variety of reasons and wanted a small project to sort of jump-start me for 2025. I had watched [Newfangled Dan ship obliterate](https://www.newfangledaudio.com/obliterate) from idea to loved plugin in November, and I thought hey can I do the same in December 2. As I wrapped up 2024, I also wanted to take account of how well our project to factor code into libraries went and make a sort of todo-list of why it was hard to build a synth. This includes some insights into the sst- libraries and the clap-wrapper 2. Another indie audio dev, who asked to not be named here in case this sucks, which it doesn't I dont think, said to a group of users in a discord in an offhand way something like "the least commercially viable thing is a 6 op full matrix pure FM synth". So my response was 1. implement one, 2. give it away, 3 ???, 4. you know the meme. 4. It seemed fun, and I thought some people would download it. 5. It had been in my head for a while and I wanted to hear how it sounded ## How to build this software your system needs cmake and a c++20 compatible compiler. We provide pre-build windows, linux, and macOS binaries at the [release page](https://github.com/baconpaul/six-sines/releases) but especially on Linux, you may want to build it yourself, since we use ubuntu 24 machines and linux doesn't really exist. So to build it, do the standard ```aiignore git clone https://github.com/baconpaul/six-sines cd six-sines git submodule update --init --recursive cmake -Bignore/build -DCMAKE_BUILD_TYPE=Release cmake --build ignore/build --config Release --target six-sines_all ``` various plugins and executables will now be scattered around ignore/build. ## I found a bug or want a feature added So this was really a one month sprint. And its pretty self contained thing. So feature requests are things I may say no to. But open an issue and chat. Or send a PR! Bugs especially let me know. But remember, "Programming an FM/PM/RM synth is hard" is not a bug in six-sines! ## Why is this a `baconpaul` and not a `surge-synthesizer` project Well you know. It's not quite up to snuff for a surge project. And its pretty idiosyncratic. I may do a few more side quest projects in 2025. Lets see. ## Some Credits Members of the surge synth team helped with pre-release testing. Large parts of the factory patch library at first release were authored by Jacky Ligon and Kinsey Dulcet, and large parts of the workflow design improvements came from Andreya and EvilDragon. Thanks folks! We use loads of open source software of course including all the sst libraries, clap and the clap wrapper (which itself includes vst3, ausdk, rtaudio, rtmidi, and more). The definitive list right now comes from the source directory. ================================================ FILE: WIN_STANDALONE.md ================================================ # Windows Standalone: Code Review & Bug Analysis **Reviewed:** `libs/clap-libs/clap-wrapper/src/detail/standalone/windows/` **Files:** `windows_standalone.cpp`, `windows_standalone.h`, `windows_standalone.manifest`, `wrapasstandalone_windows.cpp` **Date:** 2026-04-03 **Context:** Two user reports of the standalone not displaying its UI on Windows. --- ## Reported Symptoms > "I start the .exe, it opens (with no visible UI) and lurks in the taskbar, even reacting to my MIDI keyboard. If I try and 'maximize it back' by clicking, after a couple of clicks, it crashes (I never see the UI anymore)." > "I'm randomly running into problems with the windows standalone version — it's not opening the window again, unless I switch to maximised Window and back to normal window again within the link properties." The second workaround — setting the shortcut's "Run:" property to "Maximized" and then back to "Normal" — is a significant diagnostic clue (see Hypothesis B below). --- ## Architecture Summary The implementation uses a classical Win32 approach: - **`Window`** — base class wrapping an `HWND` with a lambda-based message dispatch table - **`Plugin : Window`** — extends `Window` with CLAP plugin GUI embedding, audio/MIDI setup, and the system-menu settings UI - **`Settings : Window`** — a separate child window for audio/MIDI configuration - The message loop (`run()`) is a plain `GetMessage` / `TranslateMessage` / `DispatchMessage` loop - CLAP GUI embedding: `plugin.gui->set_parent()` is called with the main `HWND`; JUCE (or whatever the plugin uses internally) creates child windows inside it --- ## Confirmed Bugs ### Bug 1 — `nCmdShow` Completely Discarded (HIGH — explains Symptom 2 workaround) **File:** `wrapasstandalone_windows.cpp:3` ```cpp int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int /* nCmdShow */) ``` `nCmdShow` is received from Windows but deliberately commented out and never used. Later, `activate()` unconditionally calls: ```cpp // windows_standalone.cpp:341 ::ShowWindow(hwnd.get(), SW_NORMAL); ``` On Windows, the shell sets `STARTUPINFO.wShowWindow` (e.g., `SW_SHOWMAXIMIZED`) when a shortcut has a "Run:" override, and the OS uses this to override the parameter on **the very first `ShowWindow` call** for that process — but only if `nCmdShow` was originally respected and passed through. Since the code always passes `SW_NORMAL`, it bypasses the OS's ability to start the window in a different state. More importantly: the correct first-show parameter (from `nCmdShow`) should be passed so that Windows can apply any saved placement the shell has established for the application. Ignoring it can cause the window to appear at unexpected sizes or positions. The fix for Symptom 2's workaround is explained by this: setting the shortcut to "Maximized" causes `SW_SHOWMAXIMIZED` to be passed via `STARTUPINFO`, which the OS **does** honour on the first `ShowWindow` call regardless of the parameter (this is a Windows-documented behaviour). The maximized state overrides any bad saved position, and "toggling back to Normal" updates the stored shortcut state — after which the window opens correctly because maximizing has forced a new, valid saved position. --- ### Bug 2 — `adjustSize(0, 0)` if `get_size()` Returns Zero (HIGH — explains Symptom 1) **File:** `windows_standalone.cpp:1109–1113` ```cpp Position pluginSize; plugin.gui->get_size(plugin.plugin, &pluginSize.width, &pluginSize.height); log("{}, {}", pluginSize.width, pluginSize.height); adjustSize(pluginSize.width, pluginSize.height); ``` `get_size()` is called immediately after `create()` and `set_scale()`, before `set_parent()`. If the plugin has not yet computed its layout at this point (e.g., it requires a visible parent or defers layout until `set_parent()` is processed), it may return `(0, 0)`. `adjustSize(0, 0)` then sets the window to zero size: ```cpp // windows_standalone.cpp:359–373 void Window::adjustSize(uint32_t width, uint32_t height) { ::RECT rect{ 0, 0, width, height }; // → {0, 0, 0, 0} // AdjustWindowRectExForDpi adds border/chrome, but with 0 inner size // the result is just the chrome, possibly sub-pixel or clipped to zero ::SetWindowPos(hwnd.get(), nullptr, 0, 0, (rect.right - rect.left), (rect.bottom - rect.top), SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE); } ``` The window exists and has a taskbar entry, but is effectively invisible. Audio and MIDI run on their own threads and work fine. Forcing the window to maximized forces a new size to be negotiated via `onRequestResize`, which is why that workaround helps. --- ### Bug 3 — `set_parent()` Called on a Hidden Parent Window (MEDIUM — may cause rendering issues) **File:** `windows_standalone.cpp:1115–1116` ```cpp clap_window clapWindow{CLAP_WINDOW_API_WIN32, static_cast(hwnd.get())}; plugin.gui->set_parent(plugin.plugin, &clapWindow); ``` `set_parent()` is called before `activate()` (line 1162). The parent `HWND` has not yet been shown (`WS_VISIBLE` is not in the initial `CreateWindowExW` style). JUCE (or any Win32-based plugin) creates its child windows into a hidden parent. When the parent is later made visible, child window painting can be deferred or in a stale state depending on how the plugin handles this. Some runtimes may need `ShowWindow` + `UpdateWindow` on the child explicitly after reparenting. --- ### Bug 4 — `hwnd.reset()` in `WM_CLOSE` Causes Dangling References (HIGH — explains crash) **File:** `windows_standalone.cpp:328–333` ```cpp if (msg == WM_CLOSE) { self->hwnd.reset(); // → DestroyWindow() called immediately return 0; // → suppresses DefWindowProc } ``` `hwnd` is a `wil::unique_hwnd`. Calling `.reset()` invokes `DestroyWindow()` synchronously from within the window procedure. This is unusual and dangerous: 1. `DestroyWindow()` causes the OS to send `WM_DESTROY` and `WM_NCDESTROY` — while still inside the `WM_CLOSE` handler. 2. The `WM_DESTROY` handler (line 994) calls `plugin.gui->destroy()`, `mainFinish()`, and `quit()`. The plugin GUI is being destroyed while the parent HWND is potentially mid-message. 3. After `hwnd.reset()`, `hwnd.get()` returns `nullptr`. Any subsequent message handler, timer callback, or queued paint that calls `hwnd.get()` will crash via a null handle passed to Win32 APIs. 4. The user's reported crash ("after a couple of clicks") is consistent with this: clicking the taskbar button of a window that is in this broken half-destroyed state generates messages (e.g., `WM_ACTIVATE`, `WM_SETFOCUS`, repaint requests) that arrive after the HWND is already invalid. The standard Win32 pattern is to let `DefWindowProcW` handle `WM_CLOSE`, which calls `DestroyWindow()` safely after the message handler returns, and only null out the stored handle in `WM_NCDESTROY`. --- ### Bug 5 — `set_size(0, 0)` Called When Window Is Minimized (MEDIUM — crash risk) **File:** `windows_standalone.cpp:668–672` ```cpp if (plugin.gui->can_resize(plugin.plugin)) { plugin.gui->adjust_size(plugin.plugin, &client.width, &client.height); plugin.gui->set_size(plugin.plugin, client.width, client.height); } ``` This runs on **every** `WM_WINDOWPOSCHANGED`, including when the window is minimized. When minimized, `GetClientRect` returns `{0, 0, 0, 0}`, so `client.width = 0` and `client.height = 0`. Calling `set_size(0, 0)` on a CLAP plugin GUI is likely undefined behaviour and may crash. A guard like `if (placement.showCmd != SW_SHOWMINIMIZED)` (the pattern already used in `onRequestResize` at line 1132) should be applied here too. --- ### Bug 6 — DPI Width Calculation Is Backwards (LOW — currently dormant) **File:** `windows_standalone.cpp:316` ```cpp self->suggested.width = rect->left - rect->right; // ← WRONG: produces negative value self->suggested.height = rect->bottom - rect->top; // ← correct ``` Should be `rect->right - rect->left`. The `suggested` field does not appear to be consumed anywhere in the current code, so this is latent. If it is ever used for `WM_DPICHANGED` repositioning, it will produce a window of negative/enormous width. --- ### Bug 7 — No Error Checking on Critical GUI Calls (MEDIUM — silent failures) **File:** `windows_standalone.cpp:1088–1116` ```cpp plugin.gui->create(plugin.plugin, CLAP_WINDOW_API_WIN32, false); // return value ignored plugin.gui->set_scale(plugin.plugin, scale); // return value ignored plugin.gui->get_size(plugin.plugin, &pluginSize.width, &pluginSize.height); // ignored plugin.gui->set_parent(plugin.plugin, &clapWindow); // return value ignored ``` All of these return `bool`. If any fails, the plugin GUI ends up in an undefined partial state. There is no log output or early exit, so the user sees a black/empty window with no diagnostic information. --- ## Hypotheses for Reported Symptoms ### Symptom 1: "No visible UI, lurks in taskbar, MIDI works" **Primary hypothesis: `adjustSize(0, 0)` due to premature `get_size()` call (Bug 2)** If the plugin's `get_size()` returns `(0, 0)` before `set_parent()` has been called, the window is set to zero client area. The frame exists (hence the taskbar entry), MIDI and audio threads are unaffected, but there is nothing to see. Forcing the window to maximize overrides the zero size, which triggers `onRequestResize` with valid dimensions, causing the plugin to properly initialize its layout. **Secondary hypothesis: Bad saved position from a disconnected monitor** The code saves the full window position on every `WM_WINDOWPOSCHANGED` and restores it unconditionally at startup (lines 1098–1101) if `width != 0 || height != 0`. If a previous session was closed with the window on a monitor that is no longer connected, the saved position could place the window entirely off-screen. The window exists and processes events but is not visible on any display. Maximizing brings it to the current screen. --- ### Symptom 2: "Doesn't open unless shortcut is set to Maximized and back" **Hypothesis: Stale off-screen saved position, cleared by the maximize-and-restore cycle** When the shortcut is set to "Maximized," `SW_SHOWMAXIMIZED` is injected via `STARTUPINFO`. Even though `activate()` calls `ShowWindow(hwnd, SW_NORMAL)`, the OS overrides the first `ShowWindow` call to use `SW_SHOWMAXIMIZED` (standard Windows behaviour for the first show). This forces the window onto the visible desktop at full-screen size. After returning the shortcut to "Normal," the previous bad saved position has been overwritten with valid screen coordinates from the maximized session. The window now opens correctly. --- ### Symptom: Crash after clicking **Primary hypothesis: Null `hwnd` dereference from Bug 4 (`hwnd.reset()` in `WM_CLOSE`)** `WM_CLOSE` destroys the window immediately via `hwnd.reset()`. Any messages queued before or during destruction (activation messages, paint requests, taskbar interaction) are then dispatched with an `hwnd.get()` that returns `nullptr`. Passing `nullptr` to APIs like `GetSystemMenu`, `SetWindowPos`, `SendMessage`, or `SetWindowLongPtrW` causes an access violation. This is the most probable crash cause given the description of "after a couple of clicks." --- ## Summary Table | # | Bug | Severity | Probable Impact | |---|-----|----------|-----------------| | 1 | `nCmdShow` ignored; `SW_NORMAL` hardcoded | High | Window ignores shell placement; Symptom 2 workaround | | 2 | `adjustSize(0,0)` if `get_size()` returns zero before `set_parent()` | High | Window is zero-size / invisible (Symptom 1) | | 3 | `set_parent()` called on hidden parent | Medium | Potential painting/embedding issues | | 4 | `hwnd.reset()` in `WM_CLOSE` → dangling HWND | High | Crash after window interaction | | 5 | `set_size(0,0)` called when minimized | Medium | Likely crash on minimize | | 6 | DPI width calc backwards (`left - right`) | Low | Currently dormant; breaks `WM_DPICHANGED` resize | | 7 | No error checking on GUI create/set_parent | Medium | Silent invisible-window failures | --- ## Recommended Fixes (Priority Order) 1. **Fix `WM_CLOSE`:** Remove `hwnd.reset()` from there; set it to `nullptr` only in `WM_NCDESTROY` and let `DefWindowProcW` call `DestroyWindow()` naturally. 2. **Fix `activate()`:** Pass `nCmdShow` through from `wWinMain` instead of hardcoding `SW_NORMAL`. Store it and use it in `activate()`. 3. **Fix `get_size()` call ordering:** Call `get_size()` *after* `set_parent()`, not before. Or validate that width/height are non-zero before calling `adjustSize()`. 4. **Guard `set_size()` on minimized:** Add `if (placement.showCmd != SW_SHOWMINIMIZED)` before the `adjust_size`/`set_size` calls in the `WM_WINDOWPOSCHANGED` handler. 5. **Fix DPI width calculation:** Change `rect->left - rect->right` to `rect->right - rect->left` at line 316. 6. **Add error checking:** Check return values of `gui->create()`, `gui->set_parent()`, and `gui->get_size()`, and log/abort gracefully on failure. 7. **Add off-screen position guard:** Before calling `setPosition(position)` to restore a saved position, verify that the position intersects at least one current monitor. If not, center the window instead. ================================================ FILE: cmake/CmakeRC.cmake ================================================ # This block is executed when generating an intermediate resource file, not when # running in CMake configure mode if(_CMRC_GENERATE_MODE) # Read in the digits file(READ "${INPUT_FILE}" bytes HEX) # Format each pair into a character literal. Heuristics seem to favor doing # the conversion in groups of five for fastest conversion string(REGEX REPLACE "(..)(..)(..)(..)(..)" "'\\\\x\\1','\\\\x\\2','\\\\x\\3','\\\\x\\4','\\\\x\\5'," chars "${bytes}") # Since we did this in groups, we have some leftovers to clean up string(LENGTH "${bytes}" n_bytes2) math(EXPR n_bytes "${n_bytes2} / 2") math(EXPR remainder "${n_bytes} % 5") # <-- '5' is the grouping count from above set(cleanup_re "$") set(cleanup_sub ) while(remainder) set(cleanup_re "(..)${cleanup_re}") set(cleanup_sub "'\\\\x\\${remainder}',${cleanup_sub}") math(EXPR remainder "${remainder} - 1") endwhile() if(NOT cleanup_re STREQUAL "$") string(REGEX REPLACE "${cleanup_re}" "${cleanup_sub}" chars "${chars}") endif() string(CONFIGURE [[ namespace { const char file_array[] = { @chars@ 0 }; } namespace cmrc { namespace @NAMESPACE@ { namespace res_chars { extern const char* const @SYMBOL@_begin = file_array; extern const char* const @SYMBOL@_end = file_array + @n_bytes@; }}} ]] code) file(WRITE "${OUTPUT_FILE}" "${code}") # Exit from the script. Nothing else needs to be processed return() endif() set(_version 2.0.0) cmake_minimum_required(VERSION 3.12) include(CMakeParseArguments) if(COMMAND cmrc_add_resource_library) if(NOT DEFINED _CMRC_VERSION OR NOT (_version STREQUAL _CMRC_VERSION)) message(WARNING "More than one CMakeRC version has been included in this project.") endif() # CMakeRC has already been included! Don't do anything return() endif() set(_CMRC_VERSION "${_version}" CACHE INTERNAL "CMakeRC version. Used for checking for conflicts") set(_CMRC_SCRIPT "${CMAKE_CURRENT_LIST_FILE}" CACHE INTERNAL "Path to CMakeRC script") function(_cmrc_normalize_path var) set(path "${${var}}") file(TO_CMAKE_PATH "${path}" path) while(path MATCHES "//") string(REPLACE "//" "/" path "${path}") endwhile() string(REGEX REPLACE "/+$" "" path "${path}") set("${var}" "${path}" PARENT_SCOPE) endfunction() get_filename_component(_inc_dir "${CMAKE_BINARY_DIR}/_cmrc/include" ABSOLUTE) set(CMRC_INCLUDE_DIR "${_inc_dir}" CACHE INTERNAL "Directory for CMakeRC include files") # Let's generate the primary include file file(MAKE_DIRECTORY "${CMRC_INCLUDE_DIR}/cmrc") set(hpp_content [==[ #ifndef CMRC_CMRC_HPP_INCLUDED #define CMRC_CMRC_HPP_INCLUDED #include #include #include #include #include #include #include #include #include #if !(defined(__EXCEPTIONS) || defined(__cpp_exceptions) || defined(_CPPUNWIND) || defined(CMRC_NO_EXCEPTIONS)) #define CMRC_NO_EXCEPTIONS 1 #endif namespace cmrc { namespace detail { struct dummy; } } #define CMRC_DECLARE(libid) \ namespace cmrc { namespace detail { \ struct dummy; \ static_assert(std::is_same::value, "CMRC_DECLARE() must only appear at the global namespace"); \ } } \ namespace cmrc { namespace libid { \ cmrc::embedded_filesystem get_filesystem(); \ } } static_assert(true, "") namespace cmrc { class file { const char* _begin = nullptr; const char* _end = nullptr; public: using iterator = const char*; using const_iterator = iterator; iterator begin() const noexcept { return _begin; } iterator cbegin() const noexcept { return _begin; } iterator end() const noexcept { return _end; } iterator cend() const noexcept { return _end; } std::size_t size() const { return static_cast(std::distance(begin(), end())); } file() = default; file(iterator beg, iterator end) noexcept : _begin(beg), _end(end) {} }; class directory_entry; namespace detail { class directory; class file_data; class file_or_directory { union _data_t { class file_data* file_data; class directory* directory; } _data; bool _is_file = true; public: explicit file_or_directory(file_data& f) { _data.file_data = &f; } explicit file_or_directory(directory& d) { _data.directory = &d; _is_file = false; } bool is_file() const noexcept { return _is_file; } bool is_directory() const noexcept { return !is_file(); } const directory& as_directory() const noexcept { assert(!is_file()); return *_data.directory; } const file_data& as_file() const noexcept { assert(is_file()); return *_data.file_data; } }; class file_data { public: const char* begin_ptr; const char* end_ptr; file_data(const file_data&) = delete; file_data(const char* b, const char* e) : begin_ptr(b), end_ptr(e) {} }; inline std::pair split_path(const std::string& path) { auto first_sep = path.find("/"); if (first_sep == path.npos) { return std::make_pair(path, ""); } else { return std::make_pair(path.substr(0, first_sep), path.substr(first_sep + 1)); } } struct created_subdirectory { class directory& directory; class file_or_directory& index_entry; }; class directory { std::list _files; std::list _dirs; std::map _index; using base_iterator = std::map::const_iterator; public: directory() = default; directory(const directory&) = delete; created_subdirectory add_subdir(std::string name) & { _dirs.emplace_back(); auto& back = _dirs.back(); auto& fod = _index.emplace(name, file_or_directory{back}).first->second; return created_subdirectory{back, fod}; } file_or_directory* add_file(std::string name, const char* begin, const char* end) & { assert(_index.find(name) == _index.end()); _files.emplace_back(begin, end); return &_index.emplace(name, file_or_directory{_files.back()}).first->second; } const file_or_directory* get(const std::string& path) const { auto pair = split_path(path); auto child = _index.find(pair.first); if (child == _index.end()) { return nullptr; } auto& entry = child->second; if (pair.second.empty()) { // We're at the end of the path return &entry; } if (entry.is_file()) { // We can't traverse into a file. Stop. return nullptr; } // Keep going down return entry.as_directory().get(pair.second); } class iterator { base_iterator _base_iter; base_iterator _end_iter; public: using value_type = directory_entry; using difference_type = std::ptrdiff_t; using pointer = const value_type*; using reference = const value_type&; using iterator_category = std::input_iterator_tag; iterator() = default; explicit iterator(base_iterator iter, base_iterator end) : _base_iter(iter), _end_iter(end) {} iterator begin() const noexcept { return *this; } iterator end() const noexcept { return iterator(_end_iter, _end_iter); } inline value_type operator*() const noexcept; bool operator==(const iterator& rhs) const noexcept { return _base_iter == rhs._base_iter; } bool operator!=(const iterator& rhs) const noexcept { return !(*this == rhs); } iterator& operator++() noexcept { ++_base_iter; return *this; } iterator operator++(int) noexcept { auto cp = *this; ++_base_iter; return cp; } }; using const_iterator = iterator; iterator begin() const noexcept { return iterator(_index.begin(), _index.end()); } iterator end() const noexcept { return iterator(); } }; inline std::string normalize_path(std::string path) { while (path.find("/") == 0) { path.erase(path.begin()); } while (!path.empty() && (path.rfind("/") == path.size() - 1)) { path.pop_back(); } auto off = path.npos; while ((off = path.find("//")) != path.npos) { path.erase(path.begin() + static_cast(off)); } return path; } using index_type = std::map; } // detail class directory_entry { std::string _fname; const detail::file_or_directory* _item; public: directory_entry() = delete; explicit directory_entry(std::string filename, const detail::file_or_directory& item) : _fname(filename) , _item(&item) {} const std::string& filename() const & { return _fname; } std::string filename() const && { return std::move(_fname); } bool is_file() const { return _item->is_file(); } bool is_directory() const { return _item->is_directory(); } }; directory_entry detail::directory::iterator::operator*() const noexcept { assert(begin() != end()); return directory_entry(_base_iter->first, _base_iter->second); } using directory_iterator = detail::directory::iterator; class embedded_filesystem { // Never-null: const cmrc::detail::index_type* _index; const detail::file_or_directory* _get(std::string path) const { path = detail::normalize_path(path); auto found = _index->find(path); if (found == _index->end()) { return nullptr; } else { return found->second; } } public: explicit embedded_filesystem(const detail::index_type& index) : _index(&index) {} file open(const std::string& path) const { auto entry_ptr = _get(path); if (!entry_ptr || !entry_ptr->is_file()) { #ifdef CMRC_NO_EXCEPTIONS fprintf(stderr, "Error no such file or directory: %s\n", path.c_str()); abort(); #else throw std::system_error(make_error_code(std::errc::no_such_file_or_directory), path); #endif } auto& dat = entry_ptr->as_file(); return file{dat.begin_ptr, dat.end_ptr}; } bool is_file(const std::string& path) const noexcept { auto entry_ptr = _get(path); return entry_ptr && entry_ptr->is_file(); } bool is_directory(const std::string& path) const noexcept { auto entry_ptr = _get(path); return entry_ptr && entry_ptr->is_directory(); } bool exists(const std::string& path) const noexcept { return !!_get(path); } directory_iterator iterate_directory(const std::string& path) const { auto entry_ptr = _get(path); if (!entry_ptr) { #ifdef CMRC_NO_EXCEPTIONS fprintf(stderr, "Error no such file or directory: %s\n", path.c_str()); abort(); #else throw std::system_error(make_error_code(std::errc::no_such_file_or_directory), path); #endif } if (!entry_ptr->is_directory()) { #ifdef CMRC_NO_EXCEPTIONS fprintf(stderr, "Error not a directory: %s\n", path.c_str()); abort(); #else throw std::system_error(make_error_code(std::errc::not_a_directory), path); #endif } return entry_ptr->as_directory().begin(); } }; } #endif // CMRC_CMRC_HPP_INCLUDED ]==]) set(cmrc_hpp "${CMRC_INCLUDE_DIR}/cmrc/cmrc.hpp" CACHE INTERNAL "") set(_generate 1) if(EXISTS "${cmrc_hpp}") file(READ "${cmrc_hpp}" _current) if(_current STREQUAL hpp_content) set(_generate 0) endif() endif() file(GENERATE OUTPUT "${cmrc_hpp}" CONTENT "${hpp_content}" CONDITION ${_generate}) add_library(cmrc-base INTERFACE) target_include_directories(cmrc-base INTERFACE $) # Signal a basic C++11 feature to require C++11. target_compile_features(cmrc-base INTERFACE cxx_nullptr) set_property(TARGET cmrc-base PROPERTY INTERFACE_CXX_EXTENSIONS OFF) add_library(cmrc::base ALIAS cmrc-base) function(cmrc_add_resource_library name) set(args ALIAS NAMESPACE TYPE) cmake_parse_arguments(ARG "" "${args}" "" "${ARGN}") # Generate the identifier for the resource library's namespace set(ns_re "[a-zA-Z_][a-zA-Z0-9_]*") if(NOT DEFINED ARG_NAMESPACE) # Check that the library name is also a valid namespace if(NOT name MATCHES "${ns_re}") message(SEND_ERROR "Library name is not a valid namespace. Specify the NAMESPACE argument") endif() set(ARG_NAMESPACE "${name}") else() if(NOT ARG_NAMESPACE MATCHES "${ns_re}") message(SEND_ERROR "NAMESPACE for ${name} is not a valid C++ namespace identifier (${ARG_NAMESPACE})") endif() endif() set(libname "${name}") # Check that type is either "STATIC" or "OBJECT", or default to "STATIC" if # not set if(NOT DEFINED ARG_TYPE) set(ARG_TYPE STATIC) elseif(NOT "${ARG_TYPE}" MATCHES "^(STATIC|OBJECT)$") message(SEND_ERROR "${ARG_TYPE} is not a valid TYPE (STATIC and OBJECT are acceptable)") set(ARG_TYPE STATIC) endif() # Generate a library with the compiled in character arrays. string(CONFIGURE [=[ #include #include #include namespace cmrc { namespace @ARG_NAMESPACE@ { namespace res_chars { // These are the files which are available in this resource library $, > } namespace { const cmrc::detail::index_type& get_root_index() { static cmrc::detail::directory root_directory_; static cmrc::detail::file_or_directory root_directory_fod{root_directory_}; static cmrc::detail::index_type root_index; root_index.emplace("", &root_directory_fod); struct dir_inl { class cmrc::detail::directory& directory; }; dir_inl root_directory_dir{root_directory_}; (void)root_directory_dir; $, > $, > return root_index; } } cmrc::embedded_filesystem get_filesystem() { static auto& index = get_root_index(); return cmrc::embedded_filesystem{index}; } } // @ARG_NAMESPACE@ } // cmrc ]=] cpp_content @ONLY) get_filename_component(libdir "${CMAKE_CURRENT_BINARY_DIR}/__cmrc_${name}" ABSOLUTE) get_filename_component(lib_tmp_cpp "${libdir}/lib_.cpp" ABSOLUTE) string(REPLACE "\n " "\n" cpp_content "${cpp_content}") file(GENERATE OUTPUT "${lib_tmp_cpp}" CONTENT "${cpp_content}") get_filename_component(libcpp "${libdir}/lib.cpp" ABSOLUTE) add_custom_command(OUTPUT "${libcpp}" DEPENDS "${lib_tmp_cpp}" "${cmrc_hpp}" COMMAND ${CMAKE_COMMAND} -E copy_if_different "${lib_tmp_cpp}" "${libcpp}" COMMENT "Generating ${name} resource loader" ) # Generate the actual static library. Each source file is just a single file # with a character array compiled in containing the contents of the # corresponding resource file. add_library(${name} ${ARG_TYPE} ${libcpp}) set_property(TARGET ${name} PROPERTY CMRC_LIBDIR "${libdir}") set_property(TARGET ${name} PROPERTY CMRC_NAMESPACE "${ARG_NAMESPACE}") target_link_libraries(${name} PUBLIC cmrc::base) set_property(TARGET ${name} PROPERTY CMRC_IS_RESOURCE_LIBRARY TRUE) if(ARG_ALIAS) add_library("${ARG_ALIAS}" ALIAS ${name}) endif() cmrc_add_resources(${name} ${ARG_UNPARSED_ARGUMENTS}) endfunction() function(_cmrc_register_dirs name dirpath) if(dirpath STREQUAL "") return() endif() # Skip this dir if we have already registered it get_target_property(registered "${name}" _CMRC_REGISTERED_DIRS) if(dirpath IN_LIST registered) return() endif() # Register the parent directory first get_filename_component(parent "${dirpath}" DIRECTORY) if(NOT parent STREQUAL "") _cmrc_register_dirs("${name}" "${parent}") endif() # Now generate the registration set_property(TARGET "${name}" APPEND PROPERTY _CMRC_REGISTERED_DIRS "${dirpath}") _cm_encode_fpath(sym "${dirpath}") if(parent STREQUAL "") set(parent_sym root_directory) else() _cm_encode_fpath(parent_sym "${parent}") endif() get_filename_component(leaf "${dirpath}" NAME) set_property( TARGET "${name}" APPEND PROPERTY CMRC_MAKE_DIRS "static auto ${sym}_dir = ${parent_sym}_dir.directory.add_subdir(\"${leaf}\")\;" "root_index.emplace(\"${dirpath}\", &${sym}_dir.index_entry)\;" ) endfunction() function(cmrc_add_resources name) get_target_property(is_reslib ${name} CMRC_IS_RESOURCE_LIBRARY) if(NOT TARGET ${name} OR NOT is_reslib) message(SEND_ERROR "cmrc_add_resources called on target '${name}' which is not an existing resource library") return() endif() set(options) set(args WHENCE PREFIX) set(list_args) cmake_parse_arguments(ARG "${options}" "${args}" "${list_args}" "${ARGN}") if(NOT ARG_WHENCE) set(ARG_WHENCE ${CMAKE_CURRENT_SOURCE_DIR}) endif() _cmrc_normalize_path(ARG_WHENCE) get_filename_component(ARG_WHENCE "${ARG_WHENCE}" ABSOLUTE) # Generate the identifier for the resource library's namespace get_target_property(lib_ns "${name}" CMRC_NAMESPACE) get_target_property(libdir ${name} CMRC_LIBDIR) get_target_property(target_dir ${name} SOURCE_DIR) file(RELATIVE_PATH reldir "${target_dir}" "${CMAKE_CURRENT_SOURCE_DIR}") if(reldir MATCHES "^\\.\\.") message(SEND_ERROR "Cannot call cmrc_add_resources in a parent directory from the resource library target") return() endif() foreach(input IN LISTS ARG_UNPARSED_ARGUMENTS) _cmrc_normalize_path(input) get_filename_component(abs_in "${input}" ABSOLUTE) # Generate a filename based on the input filename that we can put in # the intermediate directory. file(RELATIVE_PATH relpath "${ARG_WHENCE}" "${abs_in}") if(relpath MATCHES "^\\.\\.") # For now we just error on files that exist outside of the soure dir. message(SEND_ERROR "Cannot add file '${input}': File must be in a subdirectory of ${ARG_WHENCE}") continue() endif() if(DEFINED ARG_PREFIX) _cmrc_normalize_path(ARG_PREFIX) endif() if(ARG_PREFIX AND NOT ARG_PREFIX MATCHES "/$") set(ARG_PREFIX "${ARG_PREFIX}/") endif() get_filename_component(dirpath "${ARG_PREFIX}${relpath}" DIRECTORY) _cmrc_register_dirs("${name}" "${dirpath}") get_filename_component(abs_out "${libdir}/intermediate/${ARG_PREFIX}${relpath}.cpp" ABSOLUTE) # Generate a symbol name relpath the file's character array _cm_encode_fpath(sym "${relpath}") # Get the symbol name for the parent directory if(dirpath STREQUAL "") set(parent_sym root_directory) else() _cm_encode_fpath(parent_sym "${dirpath}") endif() # Generate the rule for the intermediate source file _cmrc_generate_intermediate_cpp(${lib_ns} ${sym} "${abs_out}" "${abs_in}") target_sources(${name} PRIVATE "${abs_out}") set_property(TARGET ${name} APPEND PROPERTY CMRC_EXTERN_DECLS "// Pointers to ${input}" "extern const char* const ${sym}_begin\;" "extern const char* const ${sym}_end\;" ) get_filename_component(leaf "${relpath}" NAME) set_property( TARGET ${name} APPEND PROPERTY CMRC_MAKE_FILES "root_index.emplace(" " \"${ARG_PREFIX}${relpath}\"," " ${parent_sym}_dir.directory.add_file(" " \"${leaf}\"," " res_chars::${sym}_begin," " res_chars::${sym}_end" " )" ")\;" ) endforeach() endfunction() function(_cmrc_generate_intermediate_cpp lib_ns symbol outfile infile) add_custom_command( # This is the file we will generate OUTPUT "${outfile}" # These are the primary files that affect the output DEPENDS "${infile}" "${_CMRC_SCRIPT}" COMMAND "${CMAKE_COMMAND}" -D_CMRC_GENERATE_MODE=TRUE -DNAMESPACE=${lib_ns} -DSYMBOL=${symbol} "-DINPUT_FILE=${infile}" "-DOUTPUT_FILE=${outfile}" -P "${_CMRC_SCRIPT}" COMMENT "Generating intermediate file for ${infile}" ) endfunction() function(_cm_encode_fpath var fpath) string(MAKE_C_IDENTIFIER "${fpath}" ident) string(MD5 hash "${fpath}") string(SUBSTRING "${hash}" 0 4 hash) set(${var} f_${hash}_${ident} PARENT_SCOPE) endfunction() ================================================ FILE: cmake/basic_installer_clapfirst.cmake ================================================ # A basic installer setup. # # Modified for products building clap first function(add_clapfirst_installer) set(oneValueArgs INSTALLER_TARGET ASSET_OUTPUT_DIRECTORY PRODUCT_NAME INSTALLER_PREFIX ) set(multiValueArgs TARGETS # A list of plugin formats, "CLAP" "VST3" "AUV2" ) cmake_parse_arguments(CIN "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) if ("${CIN_INSTALLER_PREFIX}" STREQUAL "") message(FATAL_ERROR "You must set INSTALLER_PREFIX in add_clapfirst_installer") endif() if ("${CIN_PRODUCT_NAME}" STREQUAL "") message(FATAL_ERROR "You must set PRODUCT_NAME in add_clapfirst_installer") endif() if ("${CIN_ASSET_OUTPUT_DIRECTORY}" STREQUAL "") message(FATAL_ERROR "You must set ASSET_OUTPUT_DIRECTORY in add_clapfirst_installer") endif() set(TGT ${CIN_INSTALLER_TARGET}) add_custom_target(${TGT}) foreach (INST ${CIN_TARGETS}) if (TARGET ${INST}) message(STATUS "Adding ${INST} to ${TGT} target deps") add_dependencies(${TGT} ${INST}) endif() endforeach() string(TIMESTAMP INST_DATE "%Y-%m-%d") set(INST_ZIP ${CIN_INSTALLER_PREFIX}-${CMAKE_SYSTEM_NAME}${INST_EXTRA_ZIP_NAME}-${INST_DATE}-${GIT_COMMIT_HASH}.zip) message(STATUS "Zip File Name is ${INST_ZIP}") message(STATUS "PRODUCT NAME is ${CIN_PRODUCT_NAME}") if (APPLE) message(STATUS "Configuring for mac installer") add_custom_command( TARGET ${TGT} POST_BUILD USES_TERMINAL VERBATIM WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMAND ${CMAKE_COMMAND} -E make_directory installer COMMAND echo ${CMAKE_SOURCE_DIR}/libs/sst/sst-plugininfra/scripts/installer_mac/make_installer.sh ${CIN_PRODUCT_NAME} ${CIN_ASSET_OUTPUT_DIR} ${CMAKE_SOURCE_DIR}/resources/installer_mac ${CMAKE_BINARY_DIR}/installer "${INST_DATE}-${GIT_COMMIT_HASH}" COMMAND ${CMAKE_SOURCE_DIR}/libs/sst/sst-plugininfra/scripts/installer_mac/make_installer.sh ${CIN_PRODUCT_NAME} ${CIN_ASSET_OUTPUT_DIRECTORY} ${CMAKE_SOURCE_DIR}/resources/installer_mac ${CMAKE_BINARY_DIR}/installer "${INST_DATE}-${GIT_COMMIT_HASH}" ) elseif (WIN32) message(STATUS "Configuring for win installer") cmake_path(REMOVE_EXTENSION INST_ZIP OUTPUT_VARIABLE WIN_INSTALLER) set(WINCOL ${CIN_ASSET_OUTPUT_DIRECTORY}/installer_copy) file(MAKE_DIRECTORY ${WINCOL}) add_custom_target(${TGT}_wincollect) foreach (INST ${CIN_TARGETS}) if (TARGET ${INST}) message(STATUS "Copying ${INST} installer copy") add_dependencies(${TGT}_wincollect ${INST}) add_custom_command(TARGET ${TGT}_wincollect POST_BUILD USES_TERMINAL COMMAND cmake -E echo "Staging " $ " to " ${WINCOL} COMMAND cmake -E copy "$" "${WINCOL}" ) endif() endforeach() add_dependencies(${TGT} ${TGT}_wincollect) add_custom_command( TARGET ${TGT} POST_BUILD USES_TERMINAL WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMAND ${CMAKE_COMMAND} -E echo Building zip for windoes COMMAND ${CMAKE_COMMAND} -E make_directory installer COMMAND 7z a -r installer/${INST_ZIP} ${WINCOL} ) if (TARGET innosetup::compiler) message(STATUS "Ejecting innosetup installer rules") add_dependencies(${TGT} innosetup::compiler) add_custom_command( TARGET ${TGT} POST_BUILD USES_TERMINAL WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMAND ${CMAKE_COMMAND} -E echo Building exe installer for windows with $ compiler COMMAND ${CMAKE_COMMAND} -E make_directory installer COMMAND innosetup::compiler /O"${CMAKE_BINARY_DIR}/installer" /F"${WIN_INSTALLER}" /DName="${PRODUCT_NAME}" /DNameCondensed="${PRODUCT_NAME}" /DVersion="${GIT_COMMIT_HASH}" /DID="2cfbe819-3947-4c10-93cc-03a2896fdfec" /DPublisher="BaconPaul" /DCLAP /DVST3 /DVST3_IS_SINGLE_FILE /DSA /DIcon="${CMAKE_SOURCE_DIR}/resources/SideQuestIcon.ico" /DBanner="${CMAKE_SOURCE_DIR}/resources/SideQuestBanner.png" /DArch="x64compatible" /DLicense="${CMAKE_SOURCE_DIR}/resources/LICENSE_GPL3" /DStagedAssets="${WINCOL}" "$" ) endif() else () message(STATUS "Basic Installer: Target is installer/${OBXF_ZIP}") add_custom_command( TARGET ${TGT} POST_BUILD WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMAND ${CMAKE_COMMAND} -E make_directory installer COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/resources/LICENSE_GPL3 ${CIN_ASSET_OUTPUT_DIRECTORY} COMMAND ${CMAKE_COMMAND} -E tar cvf installer/${INST_ZIP} --format=zip ${CIN_ASSET_OUTPUT_DIRECTORY}/ COMMAND ${CMAKE_COMMAND} -E echo "Installer in: installer/${INST_ZIP}") #add_custom_command( # TARGET obxf-installer # POST_BUILD # WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} # USES_TERMINAL # COMMAND scripts/installer_linux/make_deb.sh ${OBXF_PRODUCT_DIR} ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR}/installer "${OBXF_DATE}-${GIT_COMMIT_HASH}" #) endif () endfunction() ================================================ FILE: cmake/compile-options.cmake ================================================ if (APPLE) enable_language(OBJC) enable_language(OBJCXX) set(CMAKE_OBJC_VISIBILITY_PRESET hidden) set(CMAKE_OBJCXX_VISIBILITY_PRESET hidden) endif() set(BUILD_SHARED_LIBS OFF CACHE BOOL "Never want shared if not specified") if (${BUILD_SHARED_LIBS}) message(WARNING "You have set BUILD_SHARED_LIBS to ON. This is an untested and unlikedly to work config") endif() set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_C_VISIBILITY_PRESET hidden) set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) # Compiler specific choices if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") add_compile_options( $<$:-fsanitize=address> $<$:-fsanitize=undefined> $<$:-fno-sanitize-recover=undefined> $<$:-fsanitize-address-use-after-scope> # Fill stack/heap uninitialized locals with a repeating pattern so # uninit reads surface deterministically on macOS/Linux rather than # happening to read as zero $<$:-ftrivial-auto-var-init=pattern> $<$,$,$>:-fno-char8_t> ) add_link_options( $<$:-fsanitize=address> $<$:-fsanitize=undefined> $<$:-fno-sanitize-recover=undefined> ) if (NOT APPLE) add_compile_options($,-march=armv8-a,-march=nehalem>) endif() endif() if (WIN32) add_compile_definitions(_CRT_SECURE_NO_WARNINGS=1) endif() if (MSVC) add_compile_options( # Set source and executable charsets to UTF-8 $<$:/utf-8> # Do *not* use the new, breaking char8_t UTF-8 bits in C++20. $<$:/Zc:char8_t-> # make msvc define __cplulsplus properly $<$:/Zc:__cplusplus> ) endif() ================================================ FILE: doc/12_prompts/audio_in_prompt.md ================================================ # Audio In Feature — Prompts and Plan **Date:** 2026-04-12 --- ## User Prompts (verbatim) ### Prompt 1 OK so today we are adding audio in to six sines. I want to start with a plan-only exercise for you to present me how you would solve it, and then if i like the plan we can generate code. Go laod up the six sines skills and memories you have first. Audio in works as follows 1. The clap gets changed so that the audioPorts api responds with 1 input which is not main and is stereo. Fine. 2. The processor should have a lanczos interpolation operator from basic-blocks which interpolates from clap sample rate up to internal sample rate. If the internal sample rate changes, clear the buffer. On construction, clear the buffer. Make the Lanczos nice and big. Give it like 1024 * 10 samples worth of storage or something. On each sample the audio in wrotes to the lanczos buffer. 3. At the top of each internal block the synth engine reads the upsampled version of that lanczos buffer into an internal block siuzed resampled buffer. 4. Operator 1 - and only operator 1 - can have audio in as a wavetable. It uses the upsampled buffer from step 3 when selected. Add the new type "AUDIO_IN" as a wavetable. 5. Once operator 1 is set to audio in the rest of the engine will work fine with a couple of constraints. Op1 feedback wont work, and op1 ratio does nothing. So AUDIO_IN is special in that we dont have to run the op1 lfo, envelope, etc... on the node or the feedback node just the mix node. 6. The UI side operator 1 audio in appears last in the list after a spearator. When it appears the ratio knob deactivates (but still allows skip to select) as do all the modulation and envelope parameters and the feedback knob. So that's basically the design idea. Can you give me a plan which restates this design and how you intend to do it. ### Prompt 2 OK so a few modifications to the plan, which I'd like you to make and then restate the plan. Mostly AUDIO_IN being 21 as special. In this iteration of six sines I'm going to add several new waveforms. AUDIO_IN is this but later on I'll add a bunch of phase distortion ones. So I dont want AUDIO_IN being last to be special (but your analysis that it is streaming compatible to add is fine). So what I want to do is add a ui-side feature to sort and segment the waveform menu. We can cluster with separators and reorder. So basically a separator before TX2, before SPIKY TX and before the windows, then a separator and audio in last. We can just have a little data structure for separators and order and check that all values are in the menu. Then condition for operator 1. As to the nodes, if a node is set to AUDIO_IN other than 1, lets for now silence it but have that code there in case later we decide to let audio in be in other nodes. So restate the plan with those changes for my review please. ### Prompt 3 Great. I'd like to save my prompts and your plans. I'm going to do this often. So please save exactly verbatim the prompts I entered here along with the final version of your plan along with the date to @doc/12_prompts/audio_in_prompt.md. Like I said verbatim what I prompted in the top of the document (including this) then the output of the last version of the plan. --- ## Final Plan (Revised) ### Overview Stereo audio input is added to the plugin. Op1 (and only Op1) can be set to `AUDIO_IN` waveform mode, which replaces its synthesized waveform with the live upsampled audio input. The audio arrives at host rate, is upsampled to engine rate via a Lanczos resampler in Synth, and the upsampled block is written into `MonoValues` where `OpSource` can read it. If AUDIO_IN is selected on ops 2–5, the op silences — the code path is there for potential future expansion. `AUDIO_IN` is assigned value 21 (the current `NUM_WAVEFORMS`). Future phase-distortion waveforms will be added as 22, 23, etc. A new UI data structure governs menu display order and separators independently of the integer enum order, so `AUDIO_IN` always appears last in the menu regardless of what integer values future waveforms get. --- ### Step 1: CLAP Audio Ports (`src/clap/six-sines-clap.cpp`) **`audioPortsCount`**: Change `isInput ? 0` → `isInput ? 1`. Both single and multi-out variants report one input. **`audioPortsInfo`**: Remove the `assert(!isInput)`. Add a branch for `isInput && index == 0`: - Unique `id` (e.g. `82649`) - `name = "Audio In"` - `flags = 0` — explicitly **not** `CLAP_AUDIO_PORT_IS_MAIN` - `channel_count = 2`, `port_type = CLAP_PORT_STEREO` - `in_place_pair = CLAP_INVALID_ID` **`process()`**: Before the per-sample loop, capture input data pointers: ```cpp float *audioInL{nullptr}, *audioInR{nullptr}; if (process->audio_inputs_count > 0 && process->audio_inputs[0].data32) { audioInL = process->audio_inputs[0].data32[0]; audioInR = process->audio_inputs[0].data32[1]; } ``` Inside the per-sample loop at every `s`, push one host-rate sample into the engine's resampler: ```cpp engine->pushAudioIn(audioInL ? audioInL[s] : 0.f, audioInR ? audioInR[s] : 0.f); ``` This is unconditional on `blockPos` — every host-rate input sample is pushed as it arrives. --- ### Step 2: Engine Audio-In Resampler (`src/synth/synth.h` / `synth.cpp`) **`synth.h`** — Add members: ```cpp using audioInResampler_t = sst::basic_blocks::dsp::LanczosResampler; std::unique_ptr audioInResampler; void pushAudioIn(float L, float R); ``` `LanczosResampler` has `BUFFER_SZ = 4096` host-rate samples of internal ring storage, which is far more than the 2–8 samples of lookahead the Lanczos kernel requires. This is the "nice and big" buffer; no changes to the SST library are needed. **`synth.cpp`** — In `setSampleRate()`, after computing `hostSampleRate` and `engineSampleRate`, (re)construct the resampler — this clears it implicitly since the constructor `memset`s its buffers: ```cpp audioInResampler = std::make_unique( (float)hostSampleRate, (float)engineSampleRate); ``` **`pushAudioIn()`**: Delegates to `audioInResampler->push(L, R)`. --- ### Step 3: MonoValues Audio-In Block (`src/synth/mono_values.h`) Add to `MonoValues`: ```cpp float audioInBlock alignas(16)[blockSize]{}; // engine-rate audio in, mono mix ``` This shared buffer is updated once per engine block by `processInternal`, before any voice rendering runs. --- ### Step 4: `processInternal` — Populate Audio-In Block (`src/synth/synth.cpp`) At the top of the inner `while (generated < blockSize)` loop, before rendering voices, pull one engine-rate block: ```cpp { float aiL[blockSize], aiR[blockSize]; auto got = (int)audioInResampler->populateNext(aiL, aiR, blockSize); for (int i = 0; i < blockSize; ++i) monoValues.audioInBlock[i] = (i < got) ? (aiL[i] + aiR[i]) * 0.5f : 0.f; } ``` Mono-mixes L+R since Op1 is a mono signal source in the FM graph. Zero-fills if the resampler doesn't have enough input yet (startup transient). --- ### Step 5: Waveform Enum Extension (`src/dsp/sintable.h`) Add `AUDIO_IN` immediately before `NUM_WAVEFORMS`: ```cpp TUKEY_WINDOW = 20, AUDIO_IN = 21, // no table entry; handled as special case in OpSource NUM_WAVEFORMS = 22 ``` `AUDIO_IN = 21` is a stable streaming value. Future phase-distortion waveforms will be inserted as 22, 23, … in the enum. The UI menu (Step 7) places `AUDIO_IN` last regardless of integer order. The `SinTable::at()` function is **never called** for `AUDIO_IN` — `OpSource` early-exits before reaching table lookup. --- ### Step 6: Patch Parameter Range Update (`src/synth/patch.h`) Extend the `waveForm` parameter range by one (to include index 21). All 6 ops share the same parameter template so technically all ops can hold value 21 in state, but ops 2–5 will silence if that happens (Step 8). No `patchVersion` bump needed — this is a range expansion with no backwards-incompatible changes. The display-name for value 21 in `ParamMetaData` should be `"Audio In"`. --- ### Step 7: New `src/ui/waveform-display.h` This is a new header in `src/ui/` containing a data structure that governs the waveform menu's display order and grouping. It is the authoritative ordering source — completely independent of enum integer values. ```cpp struct WaveformMenuEntry { bool isSeparator{false}; const char *label{nullptr}; // group name for separators, display name for waveforms int waveformValue{-1}; // -1 for separators }; // Base menu — all operators inline constexpr WaveformMenuEntry waveformMenuBase[] = { {false, "Sin", SinTable::SIN}, {false, "Sin Fifth", SinTable::SIN_FIFTH}, {false, "Squarish", SinTable::SQUARISH}, {false, "Sawish", SinTable::SAWISH}, {false, "Triangle", SinTable::TRIANGLE}, {false, "Sin of Cubed", SinTable::SIN_OF_CUBED}, {true, "TX"}, {false, "TX 2", SinTable::TX2}, {false, "TX 3", SinTable::TX3}, {false, "TX 4", SinTable::TX4}, {false, "TX 5", SinTable::TX5}, {false, "TX 6", SinTable::TX6}, {false, "TX 7", SinTable::TX7}, {false, "TX 8", SinTable::TX8}, {true, "Spiky TX"}, {false, "Spiky TX 2", SinTable::SPIKY_TX2}, {false, "Spiky TX 4", SinTable::SPIKY_TX4}, {false, "Spiky TX 6", SinTable::SPIKY_TX6}, {false, "Spiky TX 8", SinTable::SPIKY_TX8}, {true, "Windows"}, {false, "Hann", SinTable::HANN_WINDOW}, {false, "Blackman-Harris", SinTable::BLACKMAN_HARRIS_WINDOW}, {false, "Half BH", SinTable::HALF_BLACKMAN_HARRIS_WINDOW}, {false, "Tukey", SinTable::TUKEY_WINDOW}, // Future phase-distortion waveforms (22+) inserted here, before Audio In }; // Op1-only extension — appended to base inline constexpr WaveformMenuEntry waveformMenuAudioIn[] = { {true, ""}, // separator before Audio In {false, "Audio In", SinTable::AUDIO_IN}, }; ``` **Helper functions** (also in this header or an associated `.cpp`): - `waveformDisplayName(int value) -> const char*` — looks up label from base + audioIn tables - `nextWaveformValue(int current, bool includeAudioIn) -> int` — finds next non-separator entry in menu order - `prevWaveformValue(int current, bool includeAudioIn) -> int` — same, backwards **Validation**: A `static_assert` or debug-build `assert` that iterates all `SinTable::WaveForm` values 0..20 and verifies each appears exactly once in `waveformMenuBase`. Similarly verify `AUDIO_IN` appears exactly once in `waveformMenuAudioIn`. This catches any future waveform being added to the enum without being added to the menu. --- ### Step 8: `OpSource` — `opIndex` Member and AUDIO_IN Mode (`src/dsp/op_source.h`) Add `int opIndex{0}` as a member of `OpSource`. It is set from `Voice` after array construction (see Step 9). At the top of `renderBlock()`, after the `!active` early-out, add: ```cpp bool isAudioIn = ((int)std::round(waveForm) == SinTable::AUDIO_IN); if (isAudioIn) { if (opIndex == 0) { for (int i = 0; i < blockSize; ++i) output[i] = monoValues.audioInBlock[i] * rmLevel[i]; } else { // AUDIO_IN only wired for Op1 currently; silence other ops memset(output, 0, sizeof(output)); } fbVal[0] = fbVal[1] = 0.f; return; } ``` Ring-mod (`rmLevel`) is still applied for Op1 — matrix connections to Op1 can still ring-modulate the audio input, which is a useful FM technique. Feedback state is zeroed to prevent stale values if the mode is toggled. All ratio/phase/envelope/LFO code is bypassed. --- ### Step 9: Voice — Set `opIndex` and Skip Feedback for Op0 Audio-In (`src/synth/voice.cpp`) In `Voice::Voice()`, after constructing `src`, set indices: ```cpp for (int i = 0; i < numOps; ++i) src[i].opIndex = i; ``` In `Voice::renderBlock()`, for operator index 0, skip `selfNode[0]` when in AUDIO_IN mode: ```cpp bool op0IsAudioIn = ((int)std::round(src[0].waveForm) == SinTable::AUDIO_IN); if (!op0IsAudioIn) { selfNode[0].renderBlock(src[0].feedbackLevel, src[0].output); } src[0].renderBlock(); ``` `matrixNode` connections FROM op0 TO ops 1–5 still run unmodified — the audio signal in `src[0].output` can modulate higher operators. `mixerNode[0]` still runs — output level/envelope/LFO for Op1 continue to work. --- ### Step 10: WavPainter Fix (`src/ui/source-sub-panel.cpp`) `WavPainter::paint()` currently calls `st.at(phase)` without checking for `AUDIO_IN`. When `waveForm == AUDIO_IN`, skip the waveform drawing and instead render a representative placeholder (e.g., a flat line or a noise-like squiggle label "Audio In"). The simplest approach: check at the top of `paint()` and early-return with a text label if `AUDIO_IN`. --- ### Step 11: UI — Waveform Selector for Op1 (`src/ui/source-sub-panel.cpp`) **Custom JogUpDownButton navigation**: After `createComponent(...)` creates `wavButton`/`wavButtonD`, override the data binding's navigation. The JogUpDownButton calls `setValueFromGUI(getValue() ± 1)` for up/down. To intercept this, either: - Set custom up/down callbacks on `wavButton` that call `nextWaveformValue`/`prevWaveformValue` from `waveform-display.h` and then call `wavButtonD->setValueFromGUI(newValue)` - Or subclass `PatchDiscrete` into a `PatchDiscreteWaveform` that overrides `setValueFromGUI` to remap through menu order Either way, the navigation skips separator entries cleanly. The display string comes from `waveformDisplayName()`. **Op1 gate for AUDIO_IN**: The `includeAudioIn` flag passed to `nextWaveformValue`/`prevWaveformValue` is `(index == 0)`. For ops 2–5, navigation never reaches value 21. **`setEnabledState()`**: Extend to check for AUDIO_IN on Op1. When `index == 0 && waveForm == AUDIO_IN`, disable: - `envToRatio`, `envToRatioFine` - `lfoToRatio`, `lfoToRatioFine` - All DAHDSR sliders and shape knobs - All LFO controls - All modulation slots - `startingPhase` (no phase in audio-in mode) - `keyTrack`, `keyTrackValue`, `keyTrackLow`, `keyTrackLowValue` (irrelevant) - `tsposeButton` (ratio is meaningless) `wavButtonD->onGuiSetValue` already calls `wavPainter->repaint()` and `wavButton->repaint()` — extend it to also call `setEnabledState()` so the greying updates immediately when AUDIO_IN is selected. Also add a notification back to `SourcePanel` so the ratio knob in the top panel greys out. The `onGuiSetValue` callback can call into the editor or panel to trigger an `updateEnabledState()` on `SourcePanel`'s knobs[0]. --- ### Step 12: Self Sub Panel — Feedback Deactivation (`src/ui/self-sub-panel.cpp`) In `SelfSubPanel::populate()` (or equivalent), add a check: when the selected operator index is 0 and `waveForm == AUDIO_IN`, call `setEnabled(false)` on the feedback slider and its label. Feedback mode is a no-op for AUDIO_IN since `selfNode[0]` is skipped entirely. --- ### Summary of Files Changed | File | Change | |------|--------| | `src/clap/six-sines-clap.cpp` | Add input port; push audio samples to engine per host sample | | `src/synth/synth.h` | Add `audioInResampler`, `pushAudioIn()` | | `src/synth/synth.cpp` | Construct resampler in `setSampleRate()`; populate `monoValues.audioInBlock` in `processInternal()` | | `src/synth/mono_values.h` | Add `audioInBlock[blockSize]` | | `src/dsp/sintable.h` | Add `AUDIO_IN = 21` before `NUM_WAVEFORMS` | | `src/synth/patch.h` | Extend waveform param range to include 21; add display name | | `src/dsp/op_source.h` | Add `opIndex`; AUDIO_IN early-exit in `renderBlock()` | | `src/synth/voice.cpp` | Set `src[i].opIndex = i`; skip `selfNode[0]` for AUDIO_IN | | `src/ui/waveform-display.h` | **New file**: menu entries, separators, nav helpers, completeness validation | | `src/ui/source-sub-panel.cpp` | Custom wavButton navigation via menu order; extend `setEnabledState()` for AUDIO_IN; fix `WavPainter` for AUDIO_IN | | `src/ui/self-sub-panel.cpp` | Disable feedback when Op1 is AUDIO_IN | | `src/ui/source-panel.h/cpp` | Add `updateOpEnabledState(int idx)` to grey out ratio knob + up/down buttons | | `src/ui/matrix-panel.h/cpp` | Add `updateSelfKnobState(int idx)` to grey out feedback level knob + power toggle | --- ## Follow-up Prompt 4 OK I reviewed it and made two small changes. There's a couple of things I want to add also 1. There's no reason to do the lanczoes resample of audio in *unless* op 1 is set to AUDIO_IN so can we check that before buildign the audio in modval and just zero it otherwise 2. If op1 is set to AUDIO_IN the op1 feedback node knob in the UI can also be disabled. So please make these changes (and add this ot the explicit prompts in the doc). Then once that is done please spin up a fresh agent, ask it to review the diffs here, and provide feedback vs plan ### Changes made for Follow-up 4 1. **`src/synth/synth.cpp` `processInternal()`**: Guard the Lanczos resampler population behind `op1IsAudioIn = ((int)std::round(patch.sourceNodes[0].waveForm.value) == SinTable::AUDIO_IN)`. When not in AUDIO_IN mode, `monoValues.audioInBlock` is zeroed instead (no resampler work done). 2. **`src/ui/matrix-panel.h/cpp`**: Added `updateSelfKnobState(int idx)` which disables `Sknobs[idx]` (the `fbLevel` knob) and `Spower[idx]` (the active toggle) when that op is AUDIO_IN. Called from `SourceSubPanel::setEnabledState()` alongside `updateOpEnabledState()`. --- ## Follow-up Prompt 5 yes please address all three of those *(Context: responding to a fresh review agent's report that flagged three gaps: DAHDSR/LFO/modulation slots not disabled in SourceSubPanel, missing menu completeness static_assert in waveform-display.h, and modulation slots not greyed.)* ### Changes made for Follow-up 5 1. **`src/ui/source-sub-panel.cpp` `setEnabledState()`**: Added `setEnabled(!isAudioIn)` for all 6 DAHDSR sliders, 3 shape knobs, trigger button, all 7 LFO controls (rate, deform, phase, shape, tempoSync, bipolar, isEnv), and all 3 modulation slots (sourceMenu, targetMenu, depthSlider). 2. **`src/ui/waveform-display.h`**: Added two compile-time `static_assert`s — one verifying every waveform value 0..`TUKEY_WINDOW` appears exactly once in `waveformMenuBase`, one verifying `AUDIO_IN` appears exactly once in `waveformMenuAudioIn`. --- ## Follow-up Prompt 6 Alright close. That disable of the knobs doesn't repaint the parent so add a call to editor->repaint. Also when the op1 feedback is knocked out we also want to knock out the envelope and envelope for that node. ### Changes made for Follow-up 6 1. **`src/ui/source-sub-panel.cpp` `setEnabledState()`**: Added `editor.repaint()` after the existing `repaint()` so the operator grid and matrix panel redraw when controls are greyed. 2. **`src/ui/self-sub-panel.cpp` `setEnabledState()`**: Extended to disable all DAHDSR sliders, shape knobs, trigger button, and all LFO controls when op1 is AUDIO_IN. Also added `editor.repaint()`. --- ## Follow-up Prompt 7 Great. Please add my other verbatim prompts to @doc/12_prompts/audio_in_prompt.md and also spin up another fresh review agent and let me know what it thinks --- ## Follow-up Prompt 8 yeah fix the clap audio push bug and then let me look by hand again. And add this to the audio_in_prompt.md also ### Changes made for Follow-up 8 1. **`src/clap/six-sines-clap.cpp` `process()`**: Changed audio input push from gated `if (audioInL && audioInR)` to unconditional `engine->pushAudioIn(audioInL ? audioInL[s] : 0.f, audioInR ? audioInR[s] : 0.f)`. This ensures the resampler always receives one sample per host frame, preventing startup transients or dropouts if a host provides null channel pointers. --- ## Follow-up Prompt 9 OK couple of things on hand review 1. When you set op1 to the source why do you multiply by rm[i]? We know that will be 1 so we can skip it 2. The static assert in waveform_display hard codes TUKEY_WINDOW. Instead what I'd like to do is make sure the result covers all of them up to NUM_WAVEFORMS - 1 *except* for AUDIO_IN, again because I plan to add more waveforms which will come after AUDIO_IN in the future. ### Changes made for Follow-up 9 1. **`src/dsp/op_source.h`**: Removed `* rmLevel[i]` from the AUDIO_IN copy loop — `output[i] = monoValues.audioInBlock[i]`. Ring-mod level is always 1.0 for op1 in AUDIO_IN mode since no matrix node assigns RM to it. 2. **`src/ui/waveform-display.h`**: Changed the base-menu `static_assert` to iterate `0..NUM_WAVEFORMS-1`, skip `AUDIO_IN`, and require every other value appears exactly once. This is forward-compatible: new waveforms added at 22, 23, etc. will automatically cause a build failure until added to the menu. --- ## Follow-up Prompt 10 OK great and one last review, and update the doc with these prompts --- ## Follow-up Prompt 11 Yeah lets fix 1 and 2. And with 2 lets add a comment in sin table that things will be easier if we keep audio in last and just update streaming version when adding table types so when I do add them you know to up streaming to n+1 and do a handler to push audio in always to the end. Make that change, add this prompt to doc, spin up a final review agent (including checking comments) and report back ### Changes made for Follow-up 11 1. **`src/ui/source-sub-panel.cpp` `setEnabledState()`**: Added `editor.selfSubPanel->setEnabledState()` call when `index == 0` and SelfSubPanel is visible, so feedback controls grey immediately if the user changes Op1's waveform while the feedback panel is open. Added `#include "self-sub-panel.h"` for the complete type. 2. **`src/ui/waveform-display.h` `WaveformPatchDiscrete::getMax()`**: For ops 2–5 (non-AUDIO_IN), now returns `NUM_WAVEFORMS - 2` instead of hardcoded `TUKEY_WINDOW`. This automatically includes any future synthesized waveforms added before AUDIO_IN without any change here. 3. **`src/dsp/sintable.h`**: Added comment block before `AUDIO_IN` explaining the streaming convention: new waveforms must be inserted before AUDIO_IN, AUDIO_IN's integer value will shift, so when adding them bump the SourceNode streaming version in `patch.h` and write a migration handler to remap the old AUDIO_IN value to the new one. ================================================ FILE: doc/12_prompts/color_picker.md ================================================ # Color Editor Feature — Prompts and Plan **Date:** 2026-04-15 --- ## User Prompts (verbatim) ### Prompt 1 OK I'm looking to add a feature to sst-jucegui to allow a color editor. I want this to be in three parts. FIRST there's a color editor in sst/jucegui/screens called ColorEditor. It takes a data structure which looks like a list of color tag strings and colors, and also takes a function of string,color. It uses that to present a panel which contains a scrollable list of those colors with a button - colored in the colour - and a text next to them. When you press the button it pops up a colour picker. The text field has the 32 bit hex code (so like #ff00ff00 for green) or 24 bit hex code (#ff0000 for red) depending on whether the 'includeAlpha' flag is set. This should be in a NamedPanel. When a color is edited it calls the callback. Fine. SECOND is a simple idiom to show that panel in a standalone window. That can be a static method in the same header probably with a return type like std::unique_ptr and with the constructor args for the picker screen. And also a version of the scren component which implements Modalbase so we can use it as an in app modal overlay. THIRD is an adapter atain in the same header which takes a style sheet and (ine one mode) a list of style keys and (in another mode) a list of color maps to style keys and updates the style sheet object. This is quite a big prompt so lets start with you laying out a plan. And also I want to keep the prompt history in @doc/12_prompts/ with the explicit prompt and either plan or summary of action just like i did. Call the file 12_prompts/color_picker.md. ### Prompt 2 I think thats right. On 4 the component can implement hover to draw a white line around the color to make it clear it is active. And yes on 1 2 and 3. For the picker though we want to use a viewport / scroll window if the color set is larger than 15 or so. So include that viewport code also. Theres one more part of the plan i want. That's to have six sines be able to open one of these from the main menu under UI and get all the editors for value bearing, base label, outlined, etc... which are the base styles we use for the knob, slider, dislplya, etc.... and do that directly on the style sheet. We will then want to soon enough serailize those using (probably) the style id, which we can do by introducint an sst/jucegui/style/Seralizer.h which goes to and from some text file for a style shet. Lets add that to the plan as steps. ### Prompt 3 Great. Actually in jucegui drop the toFile/fromFile and just have toString/fromString method. We cna handle the file io externally since we'll want to use std::filesystem and the likes in the client app. Do the file save in six sines. Modify the plan for me again with that in mind, restate the plan, and then lets look at it --- ## Plan ### Overview A single new header `include/sst/jucegui/screens/ColorEditor.h` provides: 1. **`ColorEditor`** — a `NamedPanel` subclass showing a scrollable list of named colors, each with a color swatch button and an editable hex text field. 2. **`ColorEditorModal`** — a `ModalBase` subclass that wraps a `ColorEditor` for use as an in-app overlay. 3. **`ColorEditor::asStandaloneWindow()`** — static factory for a `juce::DocumentWindow` hosting a `ColorEditor`. 4. **`ColorEditor::forStyleKeys()`** and **`ColorEditor::forColorMap()`** — static adapters that wire a `StyleSheet` to a `ColorEditor`. Everything lives in one header (matching the pattern of `AlertOrPrompt.h` and `ModalBase.h`). No new `.cpp` file is needed. --- ### Part 1: `ColorEditor` #### Data types ```cpp struct ColorEntry { std::string tag; // human-readable name shown as the row label juce::Colour color; }; using ColorChangedFn = std::function; ``` #### Class declaration ```cpp struct ColorEditor : components::NamedPanel, private juce::ChangeListener { ColorEditor(std::vector entries, ColorChangedFn callback, bool includeAlpha = false); void resized() override; // ... (private row component, listView, picker management) }; ``` `ColorEditor` inherits `NamedPanel` so it gets the styled header bar, outline, and `getContentArea()`. The title string passed to `NamedPanel("Color Editor")` is fixed but callers can call `setName()` after construction. #### Internal `RowComponent` `ListView` is used with `BRUTE_FORCE_NO_REUSE` strategy. Each row is a `RowComponent` (private inner struct): ``` [ color swatch button (32px wide) ][ hex text field (fills remainder) ] ``` - **Swatch button**: plain `juce::Component` that `paint()`s itself solid with `entry.color` and a 1px outline. `mouseDown` calls back into `ColorEditor::openPickerFor(idx, screenBounds)`. - **Hex text field**: `juce::TextEditor`, single-line. Initialized with `toHexString(entry.color)`. `onReturnKey` and `focusLost` parse the typed string with `fromHexString()`, update `entries[idx].color`, and fire `onColorChanged`. Row height: 28px. The `ListView` `getRowHeight` lambda returns 28. #### Hex string format ```cpp std::string toHexString(juce::Colour c) const; // includeAlpha=true → "#AARRGGBB" (8 hex digits, uppercase) // includeAlpha=false → "#RRGGBB" (6 hex digits, uppercase) juce::Colour fromHexString(const std::string& s) const; // Strips leading '#', prepends "FF" if !includeAlpha, // then calls juce::Colour::fromString(). Returns juce::Colours::transparentBlack on parse failure. ``` #### Color picker `ColorEditor` implements `juce::ChangeListener`. `openPickerFor(idx, screenBounds)`: 1. Creates a `juce::ColourSelector` with flags: ``` showColourAtTop | showSliders | showColourspace ``` plus `showAlphaChannel` if `includeAlpha`. 2. Sets the current colour to `entries[idx].color`. 3. Adds `*this` as a `ChangeListener`. 4. Launches via `juce::CallOutBox::launchAsynchronously(...)`. 5. Stores `activePickerIdx = idx` and a raw (unowned) pointer to the selector. `changeListenerCallback()`: - Reads `selector->getCurrentColour()`. - Calls `updateEntry(activePickerIdx, newColor)`. `updateEntry(idx, color)`: - Updates `entries[idx].color`. - Updates the row's hex text field text via the `ListView`'s assigned component. - Calls `onColorChanged(entries[idx].tag, color)`. - Calls `listView->refresh()`. --- ### Part 2: Standalone Window + Modal Overlay #### Static factory: `asStandaloneWindow` ```cpp static std::unique_ptr asStandaloneWindow( const std::string& title, std::vector entries, ColorChangedFn callback, bool includeAlpha = false, style::StyleSheet::ptr_t stylesheet = nullptr); ``` Implementation sketch: - Creates a `ColorEditor` (heap-allocated, owned by the window). - If `stylesheet` is non-null, calls `editor->setStyle(stylesheet)`. - Creates a `juce::DocumentWindow` with title, resizable, with close button. - Sets `window->setContentOwned(editor.release(), true)`. - Sets initial size to e.g. `400 × std::min(600, rowHeight * entries.size() + headerHeight + margins)`. - `window->setVisible(true)`. The returned `unique_ptr` is owned by the caller; destroying it closes the window. #### `ColorEditorModal` ```cpp struct ColorEditorModal : ModalBase { ColorEditorModal(std::vector entries, ColorEditor::ColorChangedFn callback, bool includeAlpha = false); juce::Point innerContentSize() override; // returns {500, 480} or similar void resized() override; private: std::unique_ptr editor; }; ``` `resized()` positions `editor` to fill `getContentArea()`. `innerContentSize()` returns a fixed default of `{500, 480}`; callers can subclass to override. `ColorEditorModal` is used with `ScreenHolder::displayModalOverlay()`, exactly as `AlertOrPrompt` is — the caller calls `setVisible(false)` from within the callback or a close button to dismiss. --- ### Part 3: Stylesheet Adapters Both are static factories on `ColorEditor`. The `StyleKey` type captures pointers to the long-lived constexpr statics: ```cpp struct StyleKey { const style::StyleSheet::Class& cls; const style::StyleSheet::Property& prop; std::string label; // display name; defaults to "ClassName::propName" }; ``` #### Mode 1: flat key list ```cpp static std::unique_ptr forStyleKeys( style::StyleSheet::ptr_t stylesheet, std::vector keys, bool includeAlpha = false); ``` Builds `ColorEntry` per key: `tag = key.label`, `color = stylesheet->getColour(key.cls, key.prop)`. `onColorChanged` callback: finds the matching key by tag, calls `stylesheet->setColour(key.cls, key.prop, newColor)`. #### Mode 2: named color map ```cpp struct ColorMapEntry { std::string name; // display name for this color group std::vector keys; // all style properties sharing this color }; static std::unique_ptr forColorMap( style::StyleSheet::ptr_t stylesheet, std::vector colorMap, bool includeAlpha = false); ``` Builds one `ColorEntry` per `ColorMapEntry`: `tag = entry.name`, `color = stylesheet->getColour(entry.keys[0].cls, entry.keys[0].prop)` (first key is representative). `onColorChanged` callback: finds the matching group by name, calls `stylesheet->setColour(k.cls, k.prop, newColor)` for **every** key in that group. --- ### File Summary | File | Action | |------|--------| | `include/sst/jucegui/screens/ColorEditor.h` | **New** — all three parts in one header | No `.cpp` needed; no changes to existing files beyond adding `#include` where callers use it. The header includes: - `"ModalBase.h"` (Part 2 modal) - `` - `"../components/NamedPanel.h"` - `"../components/ListView.h"` - `"../style/StyleSheet.h"` - `"../style/StyleAndSettingsConsumer.h"` --- ### Clarifications / decisions from follow-up 1. Text field: **editable** (type a hex to set). ✓ 2. Standalone default size: **400×480**. ✓ 3. `ColorEditorModal` size: **`{500, 480}`** (constructor arg not needed for now). ✓ 4. Swatch hover: bare `juce::Component`, paints solid color fill + 1px outline; on hover draws a **white border inset** to signal interactivity. No theme-style involvement. 5. **Viewport/scroll**: If the entry count exceeds ~15 rows the list must scroll. `ListView` already wraps a `Viewport` internally; the `ColorEditor` simply fills the content area of the `NamedPanel` with the `ListView` and the `ListView`'s internal `Viewport` handles scrolling automatically. No extra viewport layer needed. --- ### Part 4: Six Sines — Open Color Editor from UI Menu #### Where `SixSinesEditor::showMainMenu()` (`src/ui/six-sines-editor.cpp`) already has a "User Interface" submenu (`uim`). A new item "Color Editor…" is added there, after the Dark/Light mode separator. #### State ```cpp // in SixSinesEditor (six-sines-editor.h) std::unique_ptr colorEditorWindow; ``` Opening the window: ```cpp uim.addItem("Color Editor...", [w = juce::Component::SafePointer(this)]() { if (!w) return; if (w->colorEditorWindow && w->colorEditorWindow->isVisible()) { w->colorEditorWindow->toFront(true); return; } auto ce = sst::jucegui::screens::ColorEditor::forColorMap( w->style(), sixSinesBaseColorMap(), false); w->colorEditorWindow = sst::jucegui::screens::ColorEditor::asStandaloneWindow( "Six Sines Color Editor", std::move(ce)); w->colorEditorWindow->setVisible(true); }); ``` `sixSinesBaseColorMap()` is a free function (or static helper) in `six-sines-editor.cpp` that returns a `std::vector` covering the base styles: | Entry name | Style keys covered | |---|---| | `"Background"` | `Base::background` | | `"Background (hover)"` | `Base::background_hover` | | `"Outline"` | `Outlined::outline` | | `"Bright Outline"` | `Outlined::brightoutline` | | `"Value"` | `ValueBearing::value` | | `"Value (hover)"` | `ValueBearing::value_hover` | | `"Value Label"` | `ValueBearing::valuelabel` | | `"Value Label (hover)"` | `ValueBearing::valuelabel_hover` | | `"Value Background"` | `ValueBearing::valuebg` | | `"Gutter"` | `ValueGutter::gutter` | | `"Gutter (hover)"` | `ValueGutter::gutter_hover` | | `"Handle"` | `GraphicalHandle::handle` | | `"Handle (hover)"` | `GraphicalHandle::handle_hover` | | `"Handle Outline"` | `GraphicalHandle::handle_outline` | | `"Modulation Handle"` | `GraphicalHandle::modulation_handle` | | `"Label Color"` | `BaseLabel::labelcolor` | | `"Label Color (hover)"` | `BaseLabel::labelcolor_hover` | | `"Push Button Fill"` | `PushButton::fill` | | `"Push Button Fill (hover)"` | `PushButton::fill_hover` | | `"Push Button Fill (pressed)"` | `PushButton::fill_pressed` | When a color is changed the callback fires `style()->setColour(cls, prop, newColor)` then calls `w->repaint()` so all widgets update in real-time. The window is closed by the user (DocumentWindow close button). `SixSinesEditor` destructor naturally destroys the window via the `unique_ptr`. Note: `forColorMap` takes a `StyleSheet::ptr_t` so changes go directly into the live sheet. This is intentional — the color editor is a live preview tool. --- ### Part 5: Stylesheet Serializer (`sst/jucegui/style/Serializer.h`) #### Motivation After customizing colors with the color editor, users want to save/load their theme. The serializer writes all colour properties of a `StyleSheet` to a text file and reads them back. #### File New header: `include/sst/jucegui/style/Serializer.h` (header-only). #### Text format Simple key=value, one per line, comments with `#`: ``` # sst-jucegui color theme # format: className::propertyName=AARRGGBB base::background=FF1A1A2E base::background_hover=FF2A2A3E outlined::outline=FF444466 ... ``` Keys are `Class::cname + "::" + Property::pname`. Values are `juce::Colour::toDisplayString(true)` (always 8-char AARRGGBB, no `#` prefix in the file). #### API ```cpp namespace sst::jucegui::style { struct Serializer { // Serialize all colour properties in the sheet to text. // Font properties are skipped (not serializable this way). static std::string toString(const StyleSheet::ptr_t& sheet); // Parse text produced by toString() and apply colours to sheet. // Unknown keys are silently ignored (forward compatibility). // Returns false if the string is unparseable. static bool fromString(const StyleSheet::ptr_t& sheet, const std::string& content); }; } ``` File I/O is the responsibility of the calling application. `toString`/`fromString` deal in `std::string` only; the caller reads/writes the file using whatever mechanism fits (JUCE `File`, `std::filesystem`, etc.). #### Implementation sketch **`toString`**: Iterates `StyleSheet::allClasses` → for each class, iterates `StyleSheet::allProperties[cls]` → skips `Property::FONT` type → calls `sheet->getColour(cls, prop)` → formats line as `cname::pname=AARRGGBB`. **`fromString`**: Splits on newlines, skips `#` comment lines. For each `key=value` line: splits on `::` to get class name and remainder, splits remainder on `=` to get property name and colour hex. Searches `StyleSheet::allClasses` by `strcmp(cls->cname, classNameStr)`, then searches `StyleSheet::allProperties[cls]` by `strcmp(prop->pname, propNameStr)`. If found and `prop->type == Property::COLOUR`, calls `sheet->setColour(*cls, *prop, juce::Colour::fromString(valueStr))`. #### Six Sines integration The `ColorEditor`'s hamburger menu gets two items via `addAdditionalHamburgerComponent` — or, more simply, the `ColorEditor`'s `NamedPanel` hamburger callback is set by the caller in `six-sines-editor.cpp` to show a small popup with: - **"Save Color Theme…"** — `juce::FileChooser` for `.ssttheme` files; on success reads `Serializer::toString(style())` and writes it to the chosen file using `juce::File::replaceWithText`. - **"Load Color Theme…"** — `juce::FileChooser` to open `.ssttheme`; on success reads file text with `juce::File::loadFileAsString`, calls `Serializer::fromString(style(), text)`, then calls `repaint()` on the main editor and refreshes the color editor's list. All `juce::File` usage stays in `six-sines-editor.cpp`; `Serializer.h` never sees a file path. --- ### Updated File Summary | File | Action | |---|---| | `include/sst/jucegui/screens/ColorEditor.h` | **New** — Parts 1–3 in one header | | `include/sst/jucegui/style/Serializer.h` | **New** — Part 5 (stylesheet text serialization) | | `src/ui/six-sines-editor.h` | Add `colorEditorWindow` member | | `src/ui/six-sines-editor.cpp` | Add Color Editor menu item + `sixSinesBaseColorMap()` helper; Save/Load theme items | ================================================ FILE: doc/12_roadmap.md ================================================ # Six Sines 1.2 Roadmap I have a lot of ideas for a '1.2' version of six sines. Not 2.0. Still compatible. But expanded. Here's my rough list ## CZ-style PD features **UNDERWAY** - Implement phase remap **done** - Implement phase formant sweep + window style ## Upgrades to the Resampler, Final path, and Reconstruction - Make the upsample / downsample path have more control and better viz - Specifically have the pipeline be - Run at internal sample rate to make high bit rate clean signal - Soft drive saturator (use the cubic clamp form) with optional small drive - Add a cytomic SVF at 16khz / root 2 resonance - ZOH decimate the signal to 32khz at upper rate - Do a bitheight redicution (round(f * (1<op5, etc...) have a 10x depth control - Allow the last point of the enelope of lfo to participate in the node mod matrix - Add an 'Envelope Retrigger from Zero' mode - Ratio (Fine) as well as Ration available as target in operator matrix - Smooth the velocity source in the mod matrix to avoid jumps when using velocity as a modulator in legato modes - Add 'Linear' and 'ZOH' resampler options - Fix the TX tables to match the TX81Z more accurately; rename the prior incorrect tables to 'spiky' variants - Add a 'coarse' tune knob in addition to a 'fine' tune - Portamento Continuation allows porta to restart on release, on new voice, and so on - Fix several temposync bugs in the LFO - Add TZLinear FM and Exponential FM as modulation modes for the mod matrix, in addition to PM and RM; add scaling (abs, unipolar, normal) modes to the RM - Add 'Solo' feature to the mixer - Update the voice manager, allowing full note id support in VST3 and CLAP in legato modes - Add a collection of 'Window' style waveforms, useful for AM/RM modelling - Add an LFO Start Phase control - Fix a problem where the center voice was mis-identified in unison in some cases - User Interface - Add a suite of accesibility fixes to various controls - Add UI scaling from 75-150% - Add a light-mode skin - Allow fractional typeins on ratios - You can copy and paste nodes or node regions between nodes. - Consistent dirty flag displays in UI when patch is modified and unsaved to disk or DAW - Temposync string values reflect properly in clap parameter displays - Temposync controlls allow typein like '1/4' or '1/16.' or '1/8T' - Ratio buttons have a jog control - Add an option to reposition sources and matrices - Fix a problem with mouse wheel on macos when using an actual mouse (as opposed to trackpad) - Fix a problem with tooltip hover sticking - Fix a problem where the jog buttons on patch selector would mis-jump after saving a patch - Obey upper bound correctly on DAHDR typeins - Plugin related improvements - Add parameter smoothing to clap params - Add support for CLAP and VST3 pan, tune, and volume note expressions - Wrap all parameter changes in begin/end consistently - Call `gui::set_size` consistently on scale changes, fixing a windows reaper sizing issue - Code Improvements - Remove some troublesome uses of thread_local storage in envelopes and elsewhere - Place visual indication of a debug build in the UI - Move to a list/grid based layout; port the code back to sst-jucegui and adjust screens - Consolidate target locations when building both in pipeline and locally - Move juce LookandFeel management to the shared sst-jucegui pacakge - Substantial improvements to the performance of the LFO and Envelope classes under constant rate. - Clean up the patch load / sync mechanism to be more thread-aware - Set modsource param max to allow all mod sources. (the 'UNK2048' problem) - Implement (but leave off) the clap preset-factory mechanism. - Infrastructure - Move the macOS minimum to 10.14 - Build with docker ubuntu 20 image for linux - Add an option to disable the juce Software renderer on Windows - Fix a problem with UTF16 paths and patch loading on Windows. - Fix a problem with keyboard input in the VST3 in Studio1 and Live on Windows. - Documentation and Content - Upgrade the manual to include the wonderful videos from Taron - New patches from Videco - New patches from SiL3NC3 - Fix a few patches which were tuned off by 7 semitones ## v1.0.5, .4, .3, .2, and .1 v1.0.0-v1.0.5 contains a set of changes we found in the days after 1.0.0 - Make the minimum macos version 10.14 - Build the linux distribution with a ubuntu 20 docker image - Adjust the clap wrapper to work correctly in Logic/AUv2 at non 44.1 sample rates - Inform the clap host of param changes on reload ## v1.0.0 v1.0.0 is the first release of the synth, with core features intact ================================================ FILE: doc/manual.md ================================================ # Six Sines Manual Six Sines is a small synth based on audio rate modulation (techniques often called "Phase Modulation" and "Digitial Ring Modulation"). It has an architecture which allows lots of modulation and a synth engine with a couple of tricks which make it sound pretty good. You can download the latest release or Nightly version of Six Sines [here](https://github.com/baconpaul/six-sines/releases). Six Sines is free and open source software. You can get, study, modify, and re-use the source for it [here](https://github.com/baconpaul/six-sines). And please read the [acknowldgements](ack.md) for a list of thanks. And following in the legacy of other PM and FM synths, it is not exactly easy to program. The sort of 'symmetric/maximal' signal path design approach exacerbates this problem, as does the relatively thin manual you are reading right now. But don't fear! You can have fun with the synth anyway. ## I don't want to read a manual. Are there any good YT vids? Yes! As part of the [one synth challenge](https://www.kvraudio.com/forum/viewtopic.php?t=618178&start=45) in Feb 2025, KVR users [Taron](http://www.taron.de) made a series of videos showing how to do sound design in six sines. - [Video One - Six Sines First Steps](https://youtu.be/fP4wNFigUt4?si=uKoq_MVoYYqzQkN3) - [Video Two - Exploring AM](https://youtu.be/JU1Yzfb5U_c?si=lyaKbKRye48EvmSz) - [Video Three - FM and AM](https://www.youtube.com/watch?v=X7RTZz2G2ig) - [Video Four - Making a simple drumpset](https://www.youtube.com/watch?v=HnpFRSw-QBc) Thanks so much for these, Taron! They are great! ## How the synthesis works, basically Its a 6 operator FM synth, more or less. Each of the 6 operators can output to audio or can modulate themselves (feedback) or subsequent operators. So operator 3 can feedback its own signal or modulate operators 4, 5, and 6. Modulation of subsequent operators can be phase modulation (the 'phi' symbol) or digital ring/audio-rate-amplitude modulation (the 'A' symbol). But the real trick of sound design with the thing is the modulation architecture. Each knob on the screen represents a node with an independent envelope and LFO per voice. So each operator, operator modulation application, operator feedback, and operator level can be independently and individually modulated. Woof. That's work. But its fun! ## The Main Interface ![Six Sines Main Screen](sxsn_main.png) The Six Sines interface has a patch selector and main menu with a VU meter at the top, then presents sections for each of the core functions. The operators span the top with the knob for each being their ratio, the modulation matrix is below, the mixer to the side, and then the main output above. The macro section participates in modulation. Each knob has a 'power' button which means the node doesnt run in a voice in any capacity, and subsequently uses no CPU. ## The Visual Signal Path ![Six Sines Main Screen](sxsn_sigpath.png) Here we show the signal path for operator 3. Yellow lines show audio signals presented for auido output, and green lines show audio signals which are used for modulation. It starts at the "Ratio" node which sets the frequency of the operator. Then the audio signal travels to the matrix where it first intersects the op3 feedback node and self modulates. The resulting audio travels to the mixer where it is leveled by an independent op3 audio node, and then is mixed into the main output. The same signal travels further into the modulation matrix providing modulation for ops 4, 5, and 6. ## Inside a node ![Six Sines Main Screen Node](sxsn_sub.png) Once you click a knob to select it or edit it the bottom part of the user interface shows the modulation screen for that node. Node 'classes' (ratio, cross matrix, feedback, mixer, main) have subtly different modulation screens but the core idea is an envelope, an LFO, application depth controls, and then performance modulation areas. The performance modulation area allows you to map MIDI and Macros to the modulation and level beyond the internal modulators. ## Envelopes, Triggering, and Voice Modes The synth has two voicing modes, a Polyphonic and a Monophonic mode. In the polyhonic mode, it additionally has a per-key piano mode. These modes control how and when voices are created. In polyphonic mode with piano mode off, a voice is created on every key press. In polyphonic mode with piano mode on, a voice is created on a key press unless there is already a voice sounding on that key, in which case that voice is retriggered. In monophonic mode, a voice is created only when no voice is playing. If a subsequent key press happens, the voice is moved and retriggered. Retriggering retriggers envelopes and each enveope can trigger on one of four ways. - On voice start only. If you use this it can sound like a stuck note if you aren't careful. Its not. - On voice start or on a voice being re-keyed when not keyed (or "on gate changed" in modular speak) - On any key press - On release. In on-release mode, the envelope is gated if the voice is ungated, so resuming a gate gesture on a note will release the OnRelease envelope. The default for an envelope is 'Patch Default' which is set in the settings screen, but you can override. ## The Settings Screen Press the COG icon at the top of the UI to get the settings screen, where you can control MPE, Mono mode, pitch bend depth, and more. ## Oversampling The setting screen contains an oversampling control. The Six Sines oversampling strategy has the engine run at a fixed rate mostly independent of the host sample rate which is stored in the patch and you control. Mostly, because our resampler (a short FIR interpolator) works way better at round multiple downsampling. 2.5x is a lot better than 2.61x or such. So our oversample levels are fixed offsets from either 44.1 or 48khz. If you choose, say, "132.3/144khz" as your engine sample rate, if your host sample rate is a multiple of 44.1 we will choose the lower, and of 48 the higher. You may need to adjust oversampling in some high feedback cases. It of course burns cpu as it goes up and for most patches the default 2.5x is just fine. ## Screen Reader and Accessible Support Six Sines supports screen readers and accessible gestures, making the UI and programming model as inscrutable to these assistive technologies as it is to users with a visual display. Since the UI is quite big there's a few extra features for screen reader navigation. First, standard edit gestures should work on all controls, and I tried really hard to make sure tab order makes sense and labels are reasonable. If you find one which is wrong, please just drop a note on discord or github. The structure of the UI is that knobs (like "Op3 feedback level") a panel in the bottom 1/4 of the screen to edit the modulators and stuff. This panel arrives when you mouse click or edit the knob. A few features make this easier to navigate for a screen reader. If on a knob in the top third, `Command-A` will arm that knob (namely select the knob modulation panel in the area below) If on a knob in the top third, `Command-J` will jump to the control panel in the bottom And finally from anywhere in the UI, `Command-N` will expose a menu allowing you to focus any of the focusable top-section knobs or preset manager. ## Good Luck, and.. Good luck! Its fun. But tricky. If you want to add to this manual please do send a PR over. ================================================ FILE: libs/CMakeLists.txt ================================================ add_subdirectory(sst/sst-cmake) add_library(catch2 INTERFACE) target_include_directories(catch2 INTERFACE catch2/include) add_subdirectory(clap-libs/clap EXCLUDE_FROM_ALL) add_subdirectory(clap-libs/clap-helpers EXCLUDE_FROM_ALL) add_subdirectory(fmt EXCLUDE_FROM_ALL) add_subdirectory(sst/sst-clap-helpers) add_clap_juce_shim(JUCE_PATH "${JUCE_PATH}") add_library(simde INTERFACE) target_include_directories(simde INTERFACE simde) add_subdirectory(sst/sst-cpputils) add_subdirectory(sst/sst-basic-blocks) add_subdirectory(sst/sst-jucegui) add_subdirectory(sst/sst-voicemanager) set(SST_PLUGININFRA_PROVIDE_TINYXML ON CACHE BOOL "yesxml") set(SST_PLUGININFRA_PROVIDE_PATCHBASE ON CACHE BOOL "patchbase pls") add_subdirectory(sst/sst-plugininfra) add_library(mts-esp-client STATIC MTS-ESP/Client/libMTSClient.cpp) target_include_directories(mts-esp-client PUBLIC MTS-ESP/Client) set(CLAP_WRAPPER_DOWNLOAD_DEPENDENCIES TRUE CACHE BOOL "Get em") set(CLAP_WRAPPER_DONT_ADD_TARGETS TRUE CACHE BOOL "I'll targetize") set(CLAP_WRAPPER_BUILD_AUV2 TRUE CACHE BOOL "It's only logical") if (APPLE) # BaconPaul has jack kinda installed set(RTAUDIO_API_JACK FALSE CACHE BOOL "Not on apple") endif() add_subdirectory(clap-libs/clap-wrapper) add_subdirectory(libsamplerate) ================================================ FILE: libs/catch2/include/catch2/catch2.hpp ================================================ /* * Catch v2.13.9 * Generated: 2022-04-12 22:37:23.260201 * ---------------------------------------------------------- * This file has been merged from multiple headers. Please don't edit it directly * Copyright (c) 2022 Two Blue Cubes Ltd. All rights reserved. * * Distributed under the Boost Software License, Version 1.0. (See accompanying * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) */ #ifndef TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED #define TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED // start catch.hpp #define CATCH_VERSION_MAJOR 2 #define CATCH_VERSION_MINOR 13 #define CATCH_VERSION_PATCH 9 #ifdef __clang__ #pragma clang system_header #elif defined __GNUC__ #pragma GCC system_header #endif // start catch_suppress_warnings.h #ifdef __clang__ #ifdef __ICC // icpc defines the __clang__ macro #pragma warning(push) #pragma warning(disable : 161 1682) #else // __ICC #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpadded" #pragma clang diagnostic ignored "-Wswitch-enum" #pragma clang diagnostic ignored "-Wcovered-switch-default" #endif #elif defined __GNUC__ // Because REQUIREs trigger GCC's -Wparentheses, and because still // supported version of g++ have only buggy support for _Pragmas, // Wparentheses have to be suppressed globally. #pragma GCC diagnostic ignored "-Wparentheses" // See #674 for details #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-variable" #pragma GCC diagnostic ignored "-Wpadded" #endif // end catch_suppress_warnings.h #if defined(CATCH_CONFIG_MAIN) || defined(CATCH_CONFIG_RUNNER) #define CATCH_IMPL #define CATCH_CONFIG_ALL_PARTS #endif // In the impl file, we want to have access to all parts of the headers // Can also be used to sanely support PCHs #if defined(CATCH_CONFIG_ALL_PARTS) #define CATCH_CONFIG_EXTERNAL_INTERFACES #if defined(CATCH_CONFIG_DISABLE_MATCHERS) #undef CATCH_CONFIG_DISABLE_MATCHERS #endif #if !defined(CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER) #define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER #endif #endif #if !defined(CATCH_CONFIG_IMPL_ONLY) // start catch_platform.h // See e.g.: // https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-18.1/TargetConditionals.h.auto.html #ifdef __APPLE__ #include #if (defined(TARGET_OS_OSX) && TARGET_OS_OSX == 1) || (defined(TARGET_OS_MAC) && TARGET_OS_MAC == 1) #define CATCH_PLATFORM_MAC #elif (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) #define CATCH_PLATFORM_IPHONE #endif #elif defined(linux) || defined(__linux) || defined(__linux__) #define CATCH_PLATFORM_LINUX #elif defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER) || \ defined(__MINGW32__) #define CATCH_PLATFORM_WINDOWS #endif // end catch_platform.h #ifdef CATCH_IMPL #ifndef CLARA_CONFIG_MAIN #define CLARA_CONFIG_MAIN_NOT_DEFINED #define CLARA_CONFIG_MAIN #endif #endif // start catch_user_interfaces.h namespace Catch { unsigned int rngSeed(); } // end catch_user_interfaces.h // start catch_tag_alias_autoregistrar.h // start catch_common.h // start catch_compiler_capabilities.h // Detect a number of compiler features - by compiler // The following features are defined: // // CATCH_CONFIG_COUNTER : is the __COUNTER__ macro supported? // CATCH_CONFIG_WINDOWS_SEH : is Windows SEH supported? // CATCH_CONFIG_POSIX_SIGNALS : are POSIX signals supported? // CATCH_CONFIG_DISABLE_EXCEPTIONS : Are exceptions enabled? // **************** // Note to maintainers: if new toggles are added please document them // in configuration.md, too // **************** // In general each macro has a _NO_ form // (e.g. CATCH_CONFIG_NO_POSIX_SIGNALS) which disables the feature. // Many features, at point of detection, define an _INTERNAL_ macro, so they // can be combined, en-mass, with the _NO_ forms later. #ifdef __cplusplus #if (__cplusplus >= 201402L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201402L) #define CATCH_CPP14_OR_GREATER #endif #if (__cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) #define CATCH_CPP17_OR_GREATER #endif #endif // Only GCC compiler should be used in this block, so other compilers trying to // mask themselves as GCC should be ignored. #if defined(__GNUC__) && !defined(__clang__) && !defined(__ICC) && !defined(__CUDACC__) && \ !defined(__LCC__) #define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION _Pragma("GCC diagnostic push") #define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION _Pragma("GCC diagnostic pop") #define CATCH_INTERNAL_IGNORE_BUT_WARN(...) (void)__builtin_constant_p(__VA_ARGS__) #endif #if defined(__clang__) #define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION _Pragma("clang diagnostic push") #define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION _Pragma("clang diagnostic pop") // As of this writing, IBM XL's implementation of __builtin_constant_p has a bug // which results in calls to destructors being emitted for each temporary, // without a matching initialization. In practice, this can result in something // like `std::string::~string` being called on an uninitialized value. // // For example, this code will likely segfault under IBM XL: // ``` // REQUIRE(std::string("12") + "34" == "1234") // ``` // // Therefore, `CATCH_INTERNAL_IGNORE_BUT_WARN` is not implemented. #if !defined(__ibmxl__) && !defined(__CUDACC__) #define CATCH_INTERNAL_IGNORE_BUT_WARN(...) \ (void)__builtin_constant_p( \ __VA_ARGS__) /* NOLINT(cppcoreguidelines-pro-type-vararg, hicpp-vararg) */ #endif #define CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS \ _Pragma("clang diagnostic ignored \"-Wexit-time-destructors\"") \ _Pragma("clang diagnostic ignored \"-Wglobal-constructors\"") #define CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS \ _Pragma("clang diagnostic ignored \"-Wparentheses\"") #define CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS \ _Pragma("clang diagnostic ignored \"-Wunused-variable\"") #define CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS \ _Pragma("clang diagnostic ignored \"-Wgnu-zero-variadic-macro-arguments\"") #define CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS \ _Pragma("clang diagnostic ignored \"-Wunused-template\"") #endif // __clang__ //////////////////////////////////////////////////////////////////////////////// // Assume that non-Windows platforms support posix signals by default #if !defined(CATCH_PLATFORM_WINDOWS) #define CATCH_INTERNAL_CONFIG_POSIX_SIGNALS #endif //////////////////////////////////////////////////////////////////////////////// // We know some environments not to support full POSIX signals #if defined(__CYGWIN__) || defined(__QNX__) || defined(__EMSCRIPTEN__) || defined(__DJGPP__) #define CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS #endif #ifdef __OS400__ #define CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS #define CATCH_CONFIG_COLOUR_NONE #endif //////////////////////////////////////////////////////////////////////////////// // Android somehow still does not support std::to_string #if defined(__ANDROID__) #define CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING #define CATCH_INTERNAL_CONFIG_ANDROID_LOGWRITE #endif //////////////////////////////////////////////////////////////////////////////// // Not all Windows environments support SEH properly #if defined(__MINGW32__) #define CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH #endif //////////////////////////////////////////////////////////////////////////////// // PS4 #if defined(__ORBIS__) #define CATCH_INTERNAL_CONFIG_NO_NEW_CAPTURE #endif //////////////////////////////////////////////////////////////////////////////// // Cygwin #ifdef __CYGWIN__ // Required for some versions of Cygwin to declare gettimeofday // see: http://stackoverflow.com/questions/36901803/gettimeofday-not-declared-in-this-scope-cygwin #define _BSD_SOURCE // some versions of cygwin (most) do not support std::to_string. Use the libstd check. // https://gcc.gnu.org/onlinedocs/gcc-4.8.2/libstdc++/api/a01053_source.html line 2812-2813 #if !((__cplusplus >= 201103L) && defined(_GLIBCXX_USE_C99) && \ !defined(_GLIBCXX_HAVE_BROKEN_VSWPRINTF)) #define CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING #endif #endif // __CYGWIN__ //////////////////////////////////////////////////////////////////////////////// // Visual C++ #if defined(_MSC_VER) // Universal Windows platform does not support SEH // Or console colours (or console at all...) #if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) #define CATCH_CONFIG_COLOUR_NONE #else #define CATCH_INTERNAL_CONFIG_WINDOWS_SEH #endif #if !defined(__clang__) // Handle Clang masquerading for msvc // MSVC traditional preprocessor needs some workaround for __VA_ARGS__ // _MSVC_TRADITIONAL == 0 means new conformant preprocessor // _MSVC_TRADITIONAL == 1 means old traditional non-conformant preprocessor #if !defined(_MSVC_TRADITIONAL) || (defined(_MSVC_TRADITIONAL) && _MSVC_TRADITIONAL) #define CATCH_INTERNAL_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR #endif // MSVC_TRADITIONAL // Only do this if we're not using clang on Windows, which uses `diagnostic push` & `diagnostic pop` #define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION __pragma(warning(push)) #define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION __pragma(warning(pop)) #endif // __clang__ #endif // _MSC_VER #if defined(_REENTRANT) || defined(_MSC_VER) // Enable async processing, as -pthread is specified or no additional linking is required #define CATCH_INTERNAL_CONFIG_USE_ASYNC #endif // _MSC_VER //////////////////////////////////////////////////////////////////////////////// // Check if we are compiled with -fno-exceptions or equivalent #if defined(__EXCEPTIONS) || defined(__cpp_exceptions) || defined(_CPPUNWIND) #define CATCH_INTERNAL_CONFIG_EXCEPTIONS_ENABLED #endif //////////////////////////////////////////////////////////////////////////////// // DJGPP #ifdef __DJGPP__ #define CATCH_INTERNAL_CONFIG_NO_WCHAR #endif // __DJGPP__ //////////////////////////////////////////////////////////////////////////////// // Embarcadero C++Build #if defined(__BORLANDC__) #define CATCH_INTERNAL_CONFIG_POLYFILL_ISNAN #endif //////////////////////////////////////////////////////////////////////////////// // Use of __COUNTER__ is suppressed during code analysis in // CLion/AppCode 2017.2.x and former, because __COUNTER__ is not properly // handled by it. // Otherwise all supported compilers support COUNTER macro, // but user still might want to turn it off #if (!defined(__JETBRAINS_IDE__) || __JETBRAINS_IDE__ >= 20170300L) #define CATCH_INTERNAL_CONFIG_COUNTER #endif //////////////////////////////////////////////////////////////////////////////// // RTX is a special version of Windows that is real time. // This means that it is detected as Windows, but does not provide // the same set of capabilities as real Windows does. #if defined(UNDER_RTSS) || defined(RTX64_BUILD) #define CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH #define CATCH_INTERNAL_CONFIG_NO_ASYNC #define CATCH_CONFIG_COLOUR_NONE #endif #if !defined(_GLIBCXX_USE_C99_MATH_TR1) #define CATCH_INTERNAL_CONFIG_GLOBAL_NEXTAFTER #endif // Various stdlib support checks that require __has_include #if defined(__has_include) // Check if string_view is available and usable #if __has_include() && defined(CATCH_CPP17_OR_GREATER) #define CATCH_INTERNAL_CONFIG_CPP17_STRING_VIEW #endif // Check if optional is available and usable #if __has_include() && defined(CATCH_CPP17_OR_GREATER) #define CATCH_INTERNAL_CONFIG_CPP17_OPTIONAL #endif // __has_include() && defined(CATCH_CPP17_OR_GREATER) // Check if byte is available and usable #if __has_include() && defined(CATCH_CPP17_OR_GREATER) #include #if defined(__cpp_lib_byte) && (__cpp_lib_byte > 0) #define CATCH_INTERNAL_CONFIG_CPP17_BYTE #endif #endif // __has_include() && defined(CATCH_CPP17_OR_GREATER) // Check if variant is available and usable #if __has_include() && defined(CATCH_CPP17_OR_GREATER) #if defined(__clang__) && (__clang_major__ < 8) // work around clang bug with libstdc++ https://bugs.llvm.org/show_bug.cgi?id=31852 // fix should be in clang 8, workaround in libstdc++ 8.2 #include #if defined(__GLIBCXX__) && defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE < 9) #define CATCH_CONFIG_NO_CPP17_VARIANT #else #define CATCH_INTERNAL_CONFIG_CPP17_VARIANT #endif // defined(__GLIBCXX__) && defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE < 9) #else #define CATCH_INTERNAL_CONFIG_CPP17_VARIANT #endif // defined(__clang__) && (__clang_major__ < 8) #endif // __has_include() && defined(CATCH_CPP17_OR_GREATER) #endif // defined(__has_include) #if defined(CATCH_INTERNAL_CONFIG_COUNTER) && !defined(CATCH_CONFIG_NO_COUNTER) && \ !defined(CATCH_CONFIG_COUNTER) #define CATCH_CONFIG_COUNTER #endif #if defined(CATCH_INTERNAL_CONFIG_WINDOWS_SEH) && !defined(CATCH_CONFIG_NO_WINDOWS_SEH) && \ !defined(CATCH_CONFIG_WINDOWS_SEH) && !defined(CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH) #define CATCH_CONFIG_WINDOWS_SEH #endif // This is set by default, because we assume that unix compilers are posix-signal-compatible by // default. #if defined(CATCH_INTERNAL_CONFIG_POSIX_SIGNALS) && \ !defined(CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS) && !defined(CATCH_CONFIG_NO_POSIX_SIGNALS) && \ !defined(CATCH_CONFIG_POSIX_SIGNALS) #define CATCH_CONFIG_POSIX_SIGNALS #endif // This is set by default, because we assume that compilers with no wchar_t support are just rare // exceptions. #if !defined(CATCH_INTERNAL_CONFIG_NO_WCHAR) && !defined(CATCH_CONFIG_NO_WCHAR) && \ !defined(CATCH_CONFIG_WCHAR) #define CATCH_CONFIG_WCHAR #endif #if !defined(CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING) && \ !defined(CATCH_CONFIG_NO_CPP11_TO_STRING) && !defined(CATCH_CONFIG_CPP11_TO_STRING) #define CATCH_CONFIG_CPP11_TO_STRING #endif #if defined(CATCH_INTERNAL_CONFIG_CPP17_OPTIONAL) && !defined(CATCH_CONFIG_NO_CPP17_OPTIONAL) && \ !defined(CATCH_CONFIG_CPP17_OPTIONAL) #define CATCH_CONFIG_CPP17_OPTIONAL #endif #if defined(CATCH_INTERNAL_CONFIG_CPP17_STRING_VIEW) && \ !defined(CATCH_CONFIG_NO_CPP17_STRING_VIEW) && !defined(CATCH_CONFIG_CPP17_STRING_VIEW) #define CATCH_CONFIG_CPP17_STRING_VIEW #endif #if defined(CATCH_INTERNAL_CONFIG_CPP17_VARIANT) && !defined(CATCH_CONFIG_NO_CPP17_VARIANT) && \ !defined(CATCH_CONFIG_CPP17_VARIANT) #define CATCH_CONFIG_CPP17_VARIANT #endif #if defined(CATCH_INTERNAL_CONFIG_CPP17_BYTE) && !defined(CATCH_CONFIG_NO_CPP17_BYTE) && \ !defined(CATCH_CONFIG_CPP17_BYTE) #define CATCH_CONFIG_CPP17_BYTE #endif #if defined(CATCH_CONFIG_EXPERIMENTAL_REDIRECT) #define CATCH_INTERNAL_CONFIG_NEW_CAPTURE #endif #if defined(CATCH_INTERNAL_CONFIG_NEW_CAPTURE) && \ !defined(CATCH_INTERNAL_CONFIG_NO_NEW_CAPTURE) && !defined(CATCH_CONFIG_NO_NEW_CAPTURE) && \ !defined(CATCH_CONFIG_NEW_CAPTURE) #define CATCH_CONFIG_NEW_CAPTURE #endif #if !defined(CATCH_INTERNAL_CONFIG_EXCEPTIONS_ENABLED) && !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS) #define CATCH_CONFIG_DISABLE_EXCEPTIONS #endif #if defined(CATCH_INTERNAL_CONFIG_POLYFILL_ISNAN) && !defined(CATCH_CONFIG_NO_POLYFILL_ISNAN) && \ !defined(CATCH_CONFIG_POLYFILL_ISNAN) #define CATCH_CONFIG_POLYFILL_ISNAN #endif #if defined(CATCH_INTERNAL_CONFIG_USE_ASYNC) && !defined(CATCH_INTERNAL_CONFIG_NO_ASYNC) && \ !defined(CATCH_CONFIG_NO_USE_ASYNC) && !defined(CATCH_CONFIG_USE_ASYNC) #define CATCH_CONFIG_USE_ASYNC #endif #if defined(CATCH_INTERNAL_CONFIG_ANDROID_LOGWRITE) && \ !defined(CATCH_CONFIG_NO_ANDROID_LOGWRITE) && !defined(CATCH_CONFIG_ANDROID_LOGWRITE) #define CATCH_CONFIG_ANDROID_LOGWRITE #endif #if defined(CATCH_INTERNAL_CONFIG_GLOBAL_NEXTAFTER) && \ !defined(CATCH_CONFIG_NO_GLOBAL_NEXTAFTER) && !defined(CATCH_CONFIG_GLOBAL_NEXTAFTER) #define CATCH_CONFIG_GLOBAL_NEXTAFTER #endif // Even if we do not think the compiler has that warning, we still have // to provide a macro that can be used by the code. #if !defined(CATCH_INTERNAL_START_WARNINGS_SUPPRESSION) #define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION #endif #if !defined(CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION) #define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION #endif #if !defined(CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS) #define CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS #endif #if !defined(CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS) #define CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS #endif #if !defined(CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS) #define CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS #endif #if !defined(CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS) #define CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS #endif // The goal of this macro is to avoid evaluation of the arguments, but // still have the compiler warn on problems inside... #if !defined(CATCH_INTERNAL_IGNORE_BUT_WARN) #define CATCH_INTERNAL_IGNORE_BUT_WARN(...) #endif #if defined(__APPLE__) && defined(__apple_build_version__) && (__clang_major__ < 10) #undef CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS #elif defined(__clang__) && (__clang_major__ < 5) #undef CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS #endif #if !defined(CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS) #define CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS #endif #if defined(CATCH_CONFIG_DISABLE_EXCEPTIONS) #define CATCH_TRY if ((true)) #define CATCH_CATCH_ALL if ((false)) #define CATCH_CATCH_ANON(type) if ((false)) #else #define CATCH_TRY try #define CATCH_CATCH_ALL catch (...) #define CATCH_CATCH_ANON(type) catch (type) #endif #if defined(CATCH_INTERNAL_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR) && \ !defined(CATCH_CONFIG_NO_TRADITIONAL_MSVC_PREPROCESSOR) && \ !defined(CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR) #define CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR #endif // end catch_compiler_capabilities.h #define INTERNAL_CATCH_UNIQUE_NAME_LINE2(name, line) name##line #define INTERNAL_CATCH_UNIQUE_NAME_LINE(name, line) INTERNAL_CATCH_UNIQUE_NAME_LINE2(name, line) #ifdef CATCH_CONFIG_COUNTER #define INTERNAL_CATCH_UNIQUE_NAME(name) INTERNAL_CATCH_UNIQUE_NAME_LINE(name, __COUNTER__) #else #define INTERNAL_CATCH_UNIQUE_NAME(name) INTERNAL_CATCH_UNIQUE_NAME_LINE(name, __LINE__) #endif #include #include #include // We need a dummy global operator<< so we can bring it into Catch namespace later struct Catch_global_namespace_dummy { }; std::ostream &operator<<(std::ostream &, Catch_global_namespace_dummy); namespace Catch { struct CaseSensitive { enum Choice { Yes, No }; }; class NonCopyable { NonCopyable(NonCopyable const &) = delete; NonCopyable(NonCopyable &&) = delete; NonCopyable &operator=(NonCopyable const &) = delete; NonCopyable &operator=(NonCopyable &&) = delete; protected: NonCopyable(); virtual ~NonCopyable(); }; struct SourceLineInfo { SourceLineInfo() = delete; SourceLineInfo(char const *_file, std::size_t _line) noexcept : file(_file), line(_line) {} SourceLineInfo(SourceLineInfo const &other) = default; SourceLineInfo &operator=(SourceLineInfo const &) = default; SourceLineInfo(SourceLineInfo &&) noexcept = default; SourceLineInfo &operator=(SourceLineInfo &&) noexcept = default; bool empty() const noexcept { return file[0] == '\0'; } bool operator==(SourceLineInfo const &other) const noexcept; bool operator<(SourceLineInfo const &other) const noexcept; char const *file; std::size_t line; }; std::ostream &operator<<(std::ostream &os, SourceLineInfo const &info); // Bring in operator<< from global namespace into Catch namespace // This is necessary because the overload of operator<< above makes // lookup stop at namespace Catch using ::operator<<; // Use this in variadic streaming macros to allow // >> +StreamEndStop // as well as // >> stuff +StreamEndStop struct StreamEndStop { std::string operator+() const; }; template T const &operator+(T const &value, StreamEndStop) { return value; } } // namespace Catch #define CATCH_INTERNAL_LINEINFO \ ::Catch::SourceLineInfo(__FILE__, static_cast(__LINE__)) // end catch_common.h namespace Catch { struct RegistrarForTagAliases { RegistrarForTagAliases(char const *alias, char const *tag, SourceLineInfo const &lineInfo); }; } // end namespace Catch #define CATCH_REGISTER_TAG_ALIAS(alias, spec) \ CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS \ namespace \ { \ Catch::RegistrarForTagAliases \ INTERNAL_CATCH_UNIQUE_NAME(AutoRegisterTagAlias)(alias, spec, CATCH_INTERNAL_LINEINFO); \ } \ CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION // end catch_tag_alias_autoregistrar.h // start catch_test_registry.h // start catch_interfaces_testcase.h #include namespace Catch { class TestSpec; struct ITestInvoker { virtual void invoke() const = 0; virtual ~ITestInvoker(); }; class TestCase; struct IConfig; struct ITestCaseRegistry { virtual ~ITestCaseRegistry(); virtual std::vector const &getAllTests() const = 0; virtual std::vector const &getAllTestsSorted(IConfig const &config) const = 0; }; bool isThrowSafe(TestCase const &testCase, IConfig const &config); bool matchTest(TestCase const &testCase, TestSpec const &testSpec, IConfig const &config); std::vector filterTests(std::vector const &testCases, TestSpec const &testSpec, IConfig const &config); std::vector const &getAllTestCasesSorted(IConfig const &config); } // namespace Catch // end catch_interfaces_testcase.h // start catch_stringref.h #include #include #include #include namespace Catch { /// A non-owning string class (similar to the forthcoming std::string_view) /// Note that, because a StringRef may be a substring of another string, /// it may not be null terminated. class StringRef { public: using size_type = std::size_t; using const_iterator = const char *; private: static constexpr char const *const s_empty = ""; char const *m_start = s_empty; size_type m_size = 0; public: // construction constexpr StringRef() noexcept = default; StringRef(char const *rawChars) noexcept; constexpr StringRef(char const *rawChars, size_type size) noexcept : m_start(rawChars), m_size(size) { } StringRef(std::string const &stdString) noexcept : m_start(stdString.c_str()), m_size(stdString.size()) { } explicit operator std::string() const { return std::string(m_start, m_size); } public: // operators auto operator==(StringRef const &other) const noexcept -> bool; auto operator!=(StringRef const &other) const noexcept -> bool { return !(*this == other); } auto operator[](size_type index) const noexcept -> char { assert(index < m_size); return m_start[index]; } public: // named queries constexpr auto empty() const noexcept -> bool { return m_size == 0; } constexpr auto size() const noexcept -> size_type { return m_size; } // Returns the current start pointer. If the StringRef is not // null-terminated, throws std::domain_exception auto c_str() const -> char const *; public: // substrings and searches // Returns a substring of [start, start + length). // If start + length > size(), then the substring is [start, size()). // If start > size(), then the substring is empty. auto substr(size_type start, size_type length) const noexcept -> StringRef; // Returns the current start pointer. May not be null-terminated. auto data() const noexcept -> char const *; constexpr auto isNullTerminated() const noexcept -> bool { return m_start[m_size] == '\0'; } public: // iterators constexpr const_iterator begin() const { return m_start; } constexpr const_iterator end() const { return m_start + m_size; } }; auto operator+=(std::string &lhs, StringRef const &sr) -> std::string &; auto operator<<(std::ostream &os, StringRef const &sr) -> std::ostream &; constexpr auto operator"" _sr(char const *rawChars, std::size_t size) noexcept -> StringRef { return StringRef(rawChars, size); } } // namespace Catch constexpr auto operator"" _catch_sr(char const *rawChars, std::size_t size) noexcept -> Catch::StringRef { return Catch::StringRef(rawChars, size); } // end catch_stringref.h // start catch_preprocessor.hpp #define CATCH_RECURSION_LEVEL0(...) __VA_ARGS__ #define CATCH_RECURSION_LEVEL1(...) \ CATCH_RECURSION_LEVEL0(CATCH_RECURSION_LEVEL0(CATCH_RECURSION_LEVEL0(__VA_ARGS__))) #define CATCH_RECURSION_LEVEL2(...) \ CATCH_RECURSION_LEVEL1(CATCH_RECURSION_LEVEL1(CATCH_RECURSION_LEVEL1(__VA_ARGS__))) #define CATCH_RECURSION_LEVEL3(...) \ CATCH_RECURSION_LEVEL2(CATCH_RECURSION_LEVEL2(CATCH_RECURSION_LEVEL2(__VA_ARGS__))) #define CATCH_RECURSION_LEVEL4(...) \ CATCH_RECURSION_LEVEL3(CATCH_RECURSION_LEVEL3(CATCH_RECURSION_LEVEL3(__VA_ARGS__))) #define CATCH_RECURSION_LEVEL5(...) \ CATCH_RECURSION_LEVEL4(CATCH_RECURSION_LEVEL4(CATCH_RECURSION_LEVEL4(__VA_ARGS__))) #ifdef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR #define INTERNAL_CATCH_EXPAND_VARGS(...) __VA_ARGS__ // MSVC needs more evaluations #define CATCH_RECURSION_LEVEL6(...) \ CATCH_RECURSION_LEVEL5(CATCH_RECURSION_LEVEL5(CATCH_RECURSION_LEVEL5(__VA_ARGS__))) #define CATCH_RECURSE(...) CATCH_RECURSION_LEVEL6(CATCH_RECURSION_LEVEL6(__VA_ARGS__)) #else #define CATCH_RECURSE(...) CATCH_RECURSION_LEVEL5(__VA_ARGS__) #endif #define CATCH_REC_END(...) #define CATCH_REC_OUT #define CATCH_EMPTY() #define CATCH_DEFER(id) id CATCH_EMPTY() #define CATCH_REC_GET_END2() 0, CATCH_REC_END #define CATCH_REC_GET_END1(...) CATCH_REC_GET_END2 #define CATCH_REC_GET_END(...) CATCH_REC_GET_END1 #define CATCH_REC_NEXT0(test, next, ...) next CATCH_REC_OUT #define CATCH_REC_NEXT1(test, next) CATCH_DEFER(CATCH_REC_NEXT0)(test, next, 0) #define CATCH_REC_NEXT(test, next) CATCH_REC_NEXT1(CATCH_REC_GET_END test, next) #define CATCH_REC_LIST0(f, x, peek, ...) \ , f(x) CATCH_DEFER(CATCH_REC_NEXT(peek, CATCH_REC_LIST1))(f, peek, __VA_ARGS__) #define CATCH_REC_LIST1(f, x, peek, ...) \ , f(x) CATCH_DEFER(CATCH_REC_NEXT(peek, CATCH_REC_LIST0))(f, peek, __VA_ARGS__) #define CATCH_REC_LIST2(f, x, peek, ...) \ f(x) CATCH_DEFER(CATCH_REC_NEXT(peek, CATCH_REC_LIST1))(f, peek, __VA_ARGS__) #define CATCH_REC_LIST0_UD(f, userdata, x, peek, ...) \ , f(userdata, x) \ CATCH_DEFER(CATCH_REC_NEXT(peek, CATCH_REC_LIST1_UD))(f, userdata, peek, __VA_ARGS__) #define CATCH_REC_LIST1_UD(f, userdata, x, peek, ...) \ , f(userdata, x) \ CATCH_DEFER(CATCH_REC_NEXT(peek, CATCH_REC_LIST0_UD))(f, userdata, peek, __VA_ARGS__) #define CATCH_REC_LIST2_UD(f, userdata, x, peek, ...) \ f(userdata, x) \ CATCH_DEFER(CATCH_REC_NEXT(peek, CATCH_REC_LIST1_UD))(f, userdata, peek, __VA_ARGS__) // Applies the function macro `f` to each of the remaining parameters, inserts commas between the // results, and passes userdata as the first parameter to each invocation, e.g. CATCH_REC_LIST_UD(f, // x, a, b, c) evaluates to f(x, a), f(x, b), f(x, c) #define CATCH_REC_LIST_UD(f, userdata, ...) \ CATCH_RECURSE(CATCH_REC_LIST2_UD(f, userdata, __VA_ARGS__, ()()(), ()()(), ()()(), 0)) #define CATCH_REC_LIST(f, ...) \ CATCH_RECURSE(CATCH_REC_LIST2(f, __VA_ARGS__, ()()(), ()()(), ()()(), 0)) #define INTERNAL_CATCH_EXPAND1(param) INTERNAL_CATCH_EXPAND2(param) #define INTERNAL_CATCH_EXPAND2(...) INTERNAL_CATCH_NO##__VA_ARGS__ #define INTERNAL_CATCH_DEF(...) INTERNAL_CATCH_DEF __VA_ARGS__ #define INTERNAL_CATCH_NOINTERNAL_CATCH_DEF #define INTERNAL_CATCH_STRINGIZE(...) INTERNAL_CATCH_STRINGIZE2(__VA_ARGS__) #ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR #define INTERNAL_CATCH_STRINGIZE2(...) #__VA_ARGS__ #define INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS(param) \ INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_REMOVE_PARENS(param)) #else // MSVC is adding extra space and needs another indirection to expand // INTERNAL_CATCH_NOINTERNAL_CATCH_DEF #define INTERNAL_CATCH_STRINGIZE2(...) INTERNAL_CATCH_STRINGIZE3(__VA_ARGS__) #define INTERNAL_CATCH_STRINGIZE3(...) #__VA_ARGS__ #define INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS(param) \ (INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_REMOVE_PARENS(param)) + 1) #endif #define INTERNAL_CATCH_MAKE_NAMESPACE2(...) ns_##__VA_ARGS__ #define INTERNAL_CATCH_MAKE_NAMESPACE(name) INTERNAL_CATCH_MAKE_NAMESPACE2(name) #define INTERNAL_CATCH_REMOVE_PARENS(...) INTERNAL_CATCH_EXPAND1(INTERNAL_CATCH_DEF __VA_ARGS__) #ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR #define INTERNAL_CATCH_MAKE_TYPE_LIST2(...) \ decltype(get_wrapper()) #define INTERNAL_CATCH_MAKE_TYPE_LIST(...) \ INTERNAL_CATCH_MAKE_TYPE_LIST2(INTERNAL_CATCH_REMOVE_PARENS(__VA_ARGS__)) #else #define INTERNAL_CATCH_MAKE_TYPE_LIST2(...) \ INTERNAL_CATCH_EXPAND_VARGS( \ decltype(get_wrapper())) #define INTERNAL_CATCH_MAKE_TYPE_LIST(...) \ INTERNAL_CATCH_EXPAND_VARGS( \ INTERNAL_CATCH_MAKE_TYPE_LIST2(INTERNAL_CATCH_REMOVE_PARENS(__VA_ARGS__))) #endif #define INTERNAL_CATCH_MAKE_TYPE_LISTS_FROM_TYPES(...) \ CATCH_REC_LIST(INTERNAL_CATCH_MAKE_TYPE_LIST, __VA_ARGS__) #define INTERNAL_CATCH_REMOVE_PARENS_1_ARG(_0) INTERNAL_CATCH_REMOVE_PARENS(_0) #define INTERNAL_CATCH_REMOVE_PARENS_2_ARG(_0, _1) \ INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_1_ARG(_1) #define INTERNAL_CATCH_REMOVE_PARENS_3_ARG(_0, _1, _2) \ INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_2_ARG(_1, _2) #define INTERNAL_CATCH_REMOVE_PARENS_4_ARG(_0, _1, _2, _3) \ INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_3_ARG(_1, _2, _3) #define INTERNAL_CATCH_REMOVE_PARENS_5_ARG(_0, _1, _2, _3, _4) \ INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_4_ARG(_1, _2, _3, _4) #define INTERNAL_CATCH_REMOVE_PARENS_6_ARG(_0, _1, _2, _3, _4, _5) \ INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_5_ARG(_1, _2, _3, _4, _5) #define INTERNAL_CATCH_REMOVE_PARENS_7_ARG(_0, _1, _2, _3, _4, _5, _6) \ INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_6_ARG(_1, _2, _3, _4, _5, _6) #define INTERNAL_CATCH_REMOVE_PARENS_8_ARG(_0, _1, _2, _3, _4, _5, _6, _7) \ INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_7_ARG(_1, _2, _3, _4, _5, _6, _7) #define INTERNAL_CATCH_REMOVE_PARENS_9_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8) \ INTERNAL_CATCH_REMOVE_PARENS(_0), \ INTERNAL_CATCH_REMOVE_PARENS_8_ARG(_1, _2, _3, _4, _5, _6, _7, _8) #define INTERNAL_CATCH_REMOVE_PARENS_10_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \ INTERNAL_CATCH_REMOVE_PARENS(_0), \ INTERNAL_CATCH_REMOVE_PARENS_9_ARG(_1, _2, _3, _4, _5, _6, _7, _8, _9) #define INTERNAL_CATCH_REMOVE_PARENS_11_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \ INTERNAL_CATCH_REMOVE_PARENS(_0), \ INTERNAL_CATCH_REMOVE_PARENS_10_ARG(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10) #define INTERNAL_CATCH_VA_NARGS_IMPL(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N #define INTERNAL_CATCH_TYPE_GEN \ template struct TypeList \ { \ }; \ template constexpr auto get_wrapper() noexcept->TypeList \ { \ return {}; \ } \ template