├── PLUGIN_CONFIG ├── docs ├── Gate.png ├── Timing.png ├── Velocity.png ├── Live-Setup.png ├── Speed-bar.png ├── Speed-msec.png ├── Speed-note.png ├── solidArp-UI.png ├── starp-patcher.png ├── StudioOne-setup.png ├── solidArp-header.png ├── Linear-algorithm.png ├── Random-algorithm.png ├── Spiral-algorithm.png ├── cubase-channel-setup.png ├── Starp-patcher-settings.png ├── CHANGELOG.txt ├── BUILDING.md ├── INSTALLATION_GUIDE.md └── USER_MANUAL.md ├── .gitattributes ├── .gitmodules ├── external ├── doctest │ └── README.txt ├── CMakeLists.txt └── TinySHA1 │ └── TinySHA1.hpp ├── packaging ├── debian │ ├── copyright │ └── control └── windows │ └── plugin.iss ├── .gitignore ├── Tests ├── CMakeLists.txt └── Tests.cpp ├── CMake ├── ReadConfig.cmake ├── Findjuce.cmake ├── GetGitRevisionDescription.cmake.in └── GetGitRevisionDescription.cmake ├── Source ├── AlgorithmEnum.hpp ├── Algorithm.hpp ├── version.hpp.in ├── HashRandom.hpp ├── Algorithms │ ├── AlgoBase.hpp │ ├── RandomAlgorithm.hpp │ ├── LinearAlgorithm.hpp │ └── SpiralAlgorithm.hpp ├── Starp.hpp ├── ParamData.hpp ├── EditorComponent │ ├── HeaderComponent.hpp │ ├── AlgoChoiceComponent.hpp │ ├── AlgorithmComponent.hpp │ ├── RandomAlgoOptionsComponent.hpp │ ├── LinearAlgoOptionsComponent.hpp │ ├── SpiralAlgoOptionsComponent.hpp │ ├── AlgoChoiceComponent.cpp │ ├── OverlayComponent.hpp │ ├── AlgorithmComponent.cpp │ ├── HeaderComponent.cpp │ ├── RandomAlgoOptionsComponent.cpp │ ├── PropertyComponent.hpp │ ├── LinearAlgoOptionsComponent.cpp │ ├── SpiralAlgoOptionsComponent.cpp │ └── PropertyComponent.cpp ├── PluginEditor.hpp ├── HashRandom.cpp ├── ParamData.cpp ├── AlgorithmParameters.hpp ├── ProcessorParameters.hpp ├── position_data.hpp ├── PluginEditor.cpp ├── CMakeLists.txt ├── ProcessorParameters.cpp ├── PluginProcessor.hpp └── Processor │ ├── setup.cpp │ └── processing.cpp ├── README.md ├── .github └── workflows │ ├── user-manual.yml │ ├── linux-release.yml │ ├── win64-release.yml │ └── macos-release.yml ├── CMakeLists.txt └── project_vars.sh /PLUGIN_CONFIG: -------------------------------------------------------------------------------- 1 | SF_PROJECT=solidArp 2 | SF_VERSION=0.3.0 3 | -------------------------------------------------------------------------------- /docs/Gate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/Gate.png -------------------------------------------------------------------------------- /docs/Timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/Timing.png -------------------------------------------------------------------------------- /docs/Velocity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/Velocity.png -------------------------------------------------------------------------------- /docs/Live-Setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/Live-Setup.png -------------------------------------------------------------------------------- /docs/Speed-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/Speed-bar.png -------------------------------------------------------------------------------- /docs/Speed-msec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/Speed-msec.png -------------------------------------------------------------------------------- /docs/Speed-note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/Speed-note.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | PLUGIN_CONFIG text eol=lf 2 | 3 | *.sh text eol=lf 4 | *.ps1 text eol=crlf -------------------------------------------------------------------------------- /docs/solidArp-UI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/solidArp-UI.png -------------------------------------------------------------------------------- /docs/starp-patcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/starp-patcher.png -------------------------------------------------------------------------------- /docs/StudioOne-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/StudioOne-setup.png -------------------------------------------------------------------------------- /docs/solidArp-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/solidArp-header.png -------------------------------------------------------------------------------- /docs/Linear-algorithm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/Linear-algorithm.png -------------------------------------------------------------------------------- /docs/Random-algorithm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/Random-algorithm.png -------------------------------------------------------------------------------- /docs/Spiral-algorithm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/Spiral-algorithm.png -------------------------------------------------------------------------------- /docs/cubase-channel-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/cubase-channel-setup.png -------------------------------------------------------------------------------- /docs/Starp-patcher-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolidFuel/Arp/HEAD/docs/Starp-patcher-settings.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "common"] 2 | path = common 3 | url = https://github.com/SolidFuel/CommonComponents.git 4 | -------------------------------------------------------------------------------- /external/doctest/README.txt: -------------------------------------------------------------------------------- 1 | downloaded from https://github.com/doctest/doctest/releases/download/v2.4.11/doctest.h on 23-Oct-2023 -------------------------------------------------------------------------------- /external/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | add_library(TinySHA1 INTERFACE) 3 | target_include_directories(TinySHA1 INTERFACE TinySHA1/) 4 | 5 | add_library(doctest INTERFACE) 6 | target_include_directories(doctest INTERFACE doctest/) -------------------------------------------------------------------------------- /packaging/debian/copyright: -------------------------------------------------------------------------------- 1 | ${SF_PROJECT} 2 | 3 | Copyright: 2023 SolidFuel 4 | 5 | 6 | The entire code base may be distributed under the terms of the GNU General 7 | Public License (GPL), which appears immediately below. 8 | 9 | See /usr/share/common-licenses/GPL 10 | -------------------------------------------------------------------------------- /packaging/debian/control: -------------------------------------------------------------------------------- 1 | Package: ${SF_PROJ_LOWER} 2 | Version: ${SF_VERSION} 3 | Section: sound 4 | Priority: optional 5 | Architecture: amd64 6 | Maintainer: SolidFuel 7 | Description: VST3 plugin Arppegiator. 8 | VST3 format plugin that arppegiates the midi on its input. 9 | Usable in any software that supports the VST3 format. 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # test results 2 | Testing/ 3 | 4 | # default build directory for visual studio 5 | out/ 6 | # Visual studio configuration directory 7 | .vs/ 8 | 9 | #default build directory for vscode 10 | build/ 11 | # vscode configuration directory 12 | .vscode/ 13 | 14 | #Vim tempfile 15 | *.swp 16 | 17 | # Macs 18 | .DS_store 19 | 20 | # safety - ignore certs 21 | *.p12 22 | *.b64 23 | 24 | # other builds 25 | build-*/ 26 | 27 | # certificates 28 | *.p12 29 | *.b64 30 | -------------------------------------------------------------------------------- /Tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(UnitTestRunner VERSION 0.1) 2 | 3 | set_property(GLOBAL PROPERTY CTEST_TARGETS_ADDED 1) 4 | include(CTest) 5 | 6 | 7 | juce_add_console_app(UnitTestRunner PRODUCT_NAME "Unit Test Runner") 8 | 9 | target_sources(UnitTestRunner PRIVATE Tests.cpp) 10 | 11 | target_compile_definitions(UnitTestRunner PRIVATE 12 | JUCE_WEB_BROWSER=0 13 | JUCE_USE_CURL=0) 14 | 15 | target_link_libraries(UnitTestRunner PRIVATE 16 | doctest 17 | juce_recommended_config_flags 18 | juce_recommended_lto_flags 19 | juce_recommended_warning_flags 20 | juce_core) 21 | 22 | add_test(NAME my-tests COMMAND UnitTestRunner) 23 | -------------------------------------------------------------------------------- /CMake/ReadConfig.cmake: -------------------------------------------------------------------------------- 1 | function(Fileread cfgfile) 2 | file(READ "${cfgfile}" FileContents ) 3 | string(REPLACE "\n" ";" lines "${FileContents}") 4 | 5 | foreach(line ${lines}) 6 | string(REPLACE "=" ";" line_split ${line}) 7 | list(LENGTH line_split count) 8 | if (count LESS 2) 9 | message(STATUS "Skipping ${line}") 10 | continue() 11 | endif() 12 | list(GET line_split -1 value) 13 | string(STRIP "${value}" value) 14 | list(GET line_split 0 var_name) 15 | string(STRIP ${var_name} var_name) 16 | set(${var_name} ${value} PARENT_SCOPE) 17 | endforeach() 18 | endfunction() 19 | 20 | Fileread("PLUGIN_CONFIG") -------------------------------------------------------------------------------- /CMake/Findjuce.cmake: -------------------------------------------------------------------------------- 1 | #including CPM.cmake, a package manager: 2 | #https://github.com/TheLartians/CPM.cmake 3 | include(CPM) 4 | 5 | #Adds all the module sources so they appear correctly in the IDE 6 | set_property(GLOBAL PROPERTY USE_FOLDERS YES) 7 | option(JUCE_ENABLE_MODULE_SOURCE_GROUPS "Enable Module Source Groups" ON) 8 | 9 | #set any of these to "ON" if you want to build one of the juce examples 10 | #or extras (Projucer/AudioPluginHost, etc): 11 | option(JUCE_BUILD_EXTRAS "Build JUCE Extras" OFF) 12 | option(JUCE_BUILD_EXAMPLES "Build JUCE Examples" OFF) 13 | 14 | #Fetching JUCE from git 15 | #IF you want to instead point it to a local version, you can invoke CMake with 16 | #-DCPM_JUCE_SOURCE="Path_To_JUCE" 17 | CPMAddPackage("gh:juce-framework/JUCE#develop") -------------------------------------------------------------------------------- /Tests/Tests.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | #include 4 | 5 | template 6 | bool checkMin(T first, T second) 7 | { 8 | return juce::exactlyEqual(juce::jmin(first, second), std::min(first, second)); 9 | } 10 | 11 | template 12 | bool checkMax(T first, T second) 13 | { 14 | return juce::exactlyEqual(juce::jmax(first, second), std::max(first, second)); 15 | } 16 | 17 | TEST_CASE("Test that juce::jmin works") 18 | { 19 | REQUIRE(checkMin(5, 7)); 20 | REQUIRE(checkMin(12, 3)); 21 | REQUIRE(checkMin(5.31, 5.42)); 22 | } 23 | 24 | TEST_CASE("Test that juce::jmax works") 25 | { 26 | REQUIRE(checkMax(5, 7)); 27 | REQUIRE(checkMax(12, 3)); 28 | REQUIRE(checkMax(5.31, 5.42)); 29 | } -------------------------------------------------------------------------------- /docs/CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | VERSION 0.3.0 2023-12-10 2 | 3 | NEW NAME : solidArp 4 | 5 | [New Features] 6 | * Support for MacOS (Universal) 7 | * Installers for all supported platform 8 | + '.pkg' for MacOS 9 | + '.deb' for Linux 10 | + '.exe' for Windows 11 | 12 | VERSION 0.2.0 2023-11-10 13 | 14 | [Documentation] 15 | * NEW User Manual 16 | * Added Instructions for FL Studio and Cubase 17 | 18 | [New Features] 19 | * New Spiral Algorithms 20 | 21 | [Enhancements] 22 | * There are now 3 ways to specify the speed 23 | + Note values (range has been extended) 24 | + Milliseconds 25 | + Fractions of a Measure 26 | * Quintuplet timing has been added 27 | * Random Algorithm as gained a "Replace" option (see User Manual) 28 | 29 | [Bug Fixes] 30 | * Clean up looping in Cubase 31 | * Silence some Linux compile warnings. 32 | 33 | -------------------------------------------------------------------------------- /Source/AlgorithmEnum.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | //======================================================================== 16 | enum Algorithm { 17 | Random, 18 | AlgUp, /* deprecated */ 19 | AlgDown, /* deprecated */ 20 | Linear, 21 | Spiral, 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /Source/Algorithm.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | 16 | #include "Algorithms/AlgoBase.hpp" 17 | #include "Algorithms/RandomAlgorithm.hpp" 18 | #include "Algorithms/LinearAlgorithm.hpp" 19 | #include "Algorithms/SpiralAlgorithm.hpp" 20 | #include "AlgorithmParameters.hpp" 21 | 22 | #include "Starp.hpp" 23 | 24 | -------------------------------------------------------------------------------- /Source/version.hpp.in: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #include 14 | 15 | const std::string PLUGIN_NAME = "@PLUGIN_PRODUCT_NAME@"; 16 | const std::string PLUGIN_NAME_UPPER = "@PLUGIN_NAME_UPPER@"; 17 | 18 | const std::string GIT_HASH = "@GIT_SHORT_HASH@"; 19 | 20 | const std::string PLUGIN_VERSION = "@CMAKE_PROJECT_VERSION@"; -------------------------------------------------------------------------------- /Source/HashRandom.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include 16 | 17 | #include 18 | #include 19 | 20 | class HashRandom { 21 | std::unique_ptr rng_; 22 | 23 | void initialize(const std::uint8_t digest[20]); 24 | 25 | public: 26 | HashRandom(const std::uint8_t digest[20]); 27 | HashRandom(const std::string &category, long long key, double slot); 28 | int nextInt(int min, int max); 29 | float nextFloat(float min, float max); 30 | 31 | }; -------------------------------------------------------------------------------- /Source/Algorithms/AlgoBase.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include "../AlgorithmEnum.hpp" 16 | #include 17 | 18 | 19 | class AlgorithmBase { 20 | 21 | public: 22 | 23 | AlgorithmBase() = default; 24 | 25 | virtual int getNextNote(double timeline_slot, const juce::SortedSet ¬es, bool notes_changed) = 0; 26 | 27 | virtual void reset() { 28 | } 29 | 30 | virtual Algorithm get_algo() const = 0; 31 | 32 | virtual ~AlgorithmBase() {} 33 | 34 | }; 35 | -------------------------------------------------------------------------------- /Source/Starp.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | 14 | #pragma once 15 | 16 | #include 17 | 18 | #if !defined(SF_DEBUG) 19 | #define SF_DEBUG 1 20 | #endif 21 | 22 | #include 23 | 24 | template 25 | std::string concat(Args&&... args) { 26 | std::stringstream ss; 27 | 28 | (ss << ... << args); 29 | 30 | return ss.str(); 31 | 32 | } 33 | 34 | 35 | 36 | #if SF_DEBUG 37 | extern std::unique_ptr dbgout; 38 | #define DBGLOG(...) dbgout->logMessage(concat(__VA_ARGS__)); 39 | #else 40 | #define DBGLOG(...) 41 | #endif -------------------------------------------------------------------------------- /docs/BUILDING.md: -------------------------------------------------------------------------------- 1 | # Building 2 | 3 | ## Setup 4 | 5 | ### Linux 6 | 7 | The code supports both `g++` as well as `clang`. Others may work. 8 | 9 | Be sure to install the following packages. Below is for Debian and derived. 10 | 11 | ```bash 12 | sudo apt-get update 13 | 14 | # tools 15 | sudo apt install g++ git cmake 16 | 17 | # dev packages 18 | sudo apt install xorg-dev libasound2-dev libgl1-mesa-dev \ 19 | libcurl4-openssl-dev libwebkit2gtk-4.0-dev 20 | ``` 21 | 22 | ### MacOS 23 | 24 | Install `xcode` from the app store and CLI tool (`xcode-select --install`). 25 | 26 | You will also need to install `cmake` and `git` via homebrew. 27 | 28 | ### Windows 29 | 30 | Install the [Visual Studio Community edition] (https://visualstudio.microsoft.com/vs/community/) 31 | 32 | This bundles a version of `cmake` 33 | 34 | You will need to [install git from the website](https://git-scm.com/download/win) 35 | 36 | ## Clone, configure, build 37 | 38 | ```bash 39 | git clone https://github.com/SolidFuel/Arp.git 40 | cd Arp 41 | git submodule init 42 | git submodule update 43 | 44 | cmake -S. -Bbuild -DCMAKE_BUILD_TYPE:string=Release 45 | cmake --build build --config Release 46 | ``` 47 | 48 | The plugin can be found under `build/Source/Starp_artefacts/Release/VST3` -------------------------------------------------------------------------------- /Source/ParamData.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include 16 | 17 | #include "AlgorithmEnum.hpp" 18 | 19 | extern juce::Array AlgorithmIndexes; 20 | extern juce::StringArray AlgorithmChoices; 21 | 22 | //======================================================================== 23 | struct speed_value { 24 | juce::String name; 25 | double multiplier; 26 | }; 27 | 28 | extern juce::Array speed_parameter_values; 29 | 30 | //======================================================================== 31 | 32 | enum SpeedType { 33 | Note, Bar, MSec 34 | }; 35 | 36 | extern juce::StringArray SpeedTypes; -------------------------------------------------------------------------------- /Source/EditorComponent/HeaderComponent.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include 16 | 17 | 18 | class HeaderComponent : public juce::Component { 19 | 20 | public: 21 | 22 | HeaderComponent(); 23 | 24 | void paint(juce::Graphics&) override; 25 | void resized() override; 26 | 27 | private: 28 | juce::Label nameLabel_; 29 | 30 | juce::TextButton menuButton_; 31 | 32 | void showMenu_(); 33 | void processMenu_(int results); 34 | void showAboutBox_(); 35 | 36 | //========================================================== 37 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(HeaderComponent) 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /Source/EditorComponent/AlgoChoiceComponent.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include "../ParamData.hpp" 16 | #include 17 | 18 | #include 19 | 20 | using namespace solidfuel; 21 | 22 | 23 | class AlgoChoiceComponent : public juce::ChoicePropertyComponent { 24 | 25 | public : 26 | AlgoChoiceComponent(); 27 | 28 | 29 | virtual void setIndex(int newIndex) override; 30 | virtual int getIndex() const override; 31 | 32 | void setValue(juce::Value &ptr); 33 | 34 | void refresh() override; 35 | 36 | // Be sure you have called setValue() before calling this. 37 | void addListener(juce::Value::Listener *l); 38 | 39 | private : 40 | juce::Value value_ptr_; 41 | ValueListener listener_; 42 | 43 | std::map value_map_; 44 | 45 | 46 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solidArp 2 | 3 | Stable Arpeggiator VST 4 | 5 | **NEW** A [User Manual](docs/USER_MANUAL.md) is available 6 | 7 | Most random arpeggiators do not give you the same pattern every time through. 8 | 9 | This one does. 10 | 11 | You can get it to randomize the velocity and gate as well. Those patterns will 12 | also be stable. 13 | 14 | Two other sets of algorithms are available: 15 | 16 | - Linear - Up, Down, Zigzag 17 | - Spiral - In, Out, InOut, OutIn 18 | 19 | ![solidArp UI](docs/solidArp-UI.png) 20 | 21 | ## Building 22 | 23 | See [BUILDING doc](docs/BUILDING.md) 24 | 25 | ## Installing 26 | 27 | See [Installation Guide](docs/INSTALLATION_GUIDE.md) 28 | 29 | ## Technology 30 | 31 | - [TinySHA1](https://github.com/mohaps/TinySHA1/) 32 | - [JUCE](https://juce.com/) 33 | - [DocTest](https://github.com/doctest/doctest) 34 | 35 | ## License/Copyright 36 | 37 | solidArp - Stable Random Arpeggiator Plugin Copyright (C) 2023 Solid Fuel 38 | 39 | This program is free software: you can redistribute it and/or modify it under the 40 | terms of the **GNU General Public License** as published by the Free Software 41 | Foundation, either version 3 of the License, or (at your option) any later 42 | version. This program is distributed in the hope that it will be useful, but 43 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 44 | FITNESS FOR A PARTICULAR PURPOSE. 45 | 46 | See [the license file](LICENSE) for more details. 47 | -------------------------------------------------------------------------------- /Source/PluginEditor.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include "PluginProcessor.hpp" 16 | #include "ParamData.hpp" 17 | #include "EditorComponent/HeaderComponent.hpp" 18 | #include "EditorComponent/AlgorithmComponent.hpp" 19 | #include "EditorComponent/PropertyComponent.hpp" 20 | 21 | //============================================================================== 22 | class PluginEditor : public juce::AudioProcessorEditor 23 | { 24 | public: 25 | explicit PluginEditor (PluginProcessor&); 26 | ~PluginEditor() override; 27 | 28 | //============================================================================== 29 | void paint (juce::Graphics&) override; 30 | void resized() override; 31 | 32 | 33 | private: 34 | 35 | //PluginProcessor& proc_; 36 | 37 | HeaderComponent header_component_; 38 | AlgorithmComponent algorithm_component_; 39 | PropertyComponent property_component_; 40 | 41 | 42 | //========================================================== 43 | 44 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginEditor) 45 | }; 46 | -------------------------------------------------------------------------------- /CMake/GetGitRevisionDescription.cmake.in: -------------------------------------------------------------------------------- 1 | # 2 | # Internal file for GetGitRevisionDescription.cmake 3 | # 4 | # Requires CMake 2.6 or newer (uses the 'function' command) 5 | # 6 | # Original Author: 7 | # 2009-2023 Ryan Pavlik 8 | # http://academic.cleardefinition.com 9 | # Iowa State University HCI Graduate Program/VRAC 10 | # 11 | # Copyright 2009-2012, Iowa State University 12 | # Copyright 2011-2023, Contributors 13 | # Distributed under the Boost Software License, Version 1.0. 14 | # (See accompanying file LICENSE_1_0.txt or copy at 15 | # http://www.boost.org/LICENSE_1_0.txt) 16 | # SPDX-License-Identifier: BSL-1.0 17 | 18 | set(HEAD_HASH) 19 | 20 | file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024) 21 | 22 | string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS) 23 | if(HEAD_CONTENTS MATCHES "ref") 24 | # named branch 25 | string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}") 26 | if(EXISTS "@GIT_DIR@/${HEAD_REF}") 27 | configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY) 28 | else() 29 | if(EXISTS "@GIT_DIR@/packed-refs") 30 | configure_file("@GIT_DIR@/packed-refs" "@GIT_DATA@/packed-refs" 31 | COPYONLY) 32 | file(READ "@GIT_DATA@/packed-refs" PACKED_REFS) 33 | if(${PACKED_REFS} MATCHES "([0-9a-z]*) ${HEAD_REF}") 34 | set(HEAD_HASH "${CMAKE_MATCH_1}") 35 | endif() 36 | endif() 37 | endif() 38 | else() 39 | # detached HEAD 40 | configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY) 41 | endif() 42 | 43 | if(NOT HEAD_HASH) 44 | file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024) 45 | string(STRIP "${HEAD_HASH}" HEAD_HASH) 46 | endif() -------------------------------------------------------------------------------- /packaging/windows/plugin.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "${SF_PROJECT}" 5 | #define MyAppVersion "${SF_VERSION}" 6 | #define MyAppPublisher "SolidFuel" 7 | #define MyAppURL "https://www.github.com/SolidFuel" 8 | #define MyAppLower "${SF_PROJ_LOWER} 9 | 10 | 11 | [Setup] 12 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 13 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 14 | ; New-Guid in powershell 15 | AppId={{6B4E07DA-A8E1-4F8B-83A2-B42EB3CCD1AA} 16 | AppName={#MyAppName} 17 | AppVersion={#MyAppVersion} 18 | ;AppVerName={#MyAppName} {#MyAppVersion} 19 | AppPublisher={#MyAppPublisher} 20 | AppPublisherURL={#MyAppURL} 21 | AppSupportURL={#MyAppURL} 22 | AppUpdatesURL={#MyAppURL} 23 | CreateAppDir=no 24 | ; currently only support intel 64 bit 25 | ArchitecturesInstallIn64BitMode=x64 26 | ArchitecturesAllowed=x64 27 | 28 | DefaultGroupName={#MyAppName} 29 | AllowNoIcons=yes 30 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 31 | ;PrivilegesRequired=lowest 32 | OutputDir={#srcdir}\build 33 | OutputBaseFilename={#MyAppLower} 34 | Compression=lzma 35 | SolidCompression=yes 36 | WizardStyle=modern 37 | 38 | [Languages] 39 | Name: "english"; MessagesFile: "compiler:Default.isl" 40 | 41 | [Files] 42 | Source: "{#srcdir}\build\Source\{#MyAppName}_artefacts\Release\VST3\{#MyAppName}.vst3\Contents\x86_64-win\{#MyAppName}.vst3"; DestDir: "{autocf}\VST3\SolidFuel"; Flags: ignoreversion 43 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 44 | 45 | -------------------------------------------------------------------------------- /Source/EditorComponent/AlgorithmComponent.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | 16 | #include "AlgoChoiceComponent.hpp" 17 | #include "RandomAlgoOptionsComponent.hpp" 18 | #include "LinearAlgoOptionsComponent.hpp" 19 | #include "SpiralAlgoOptionsComponent.hpp" 20 | 21 | #include "../ProcessorParameters.hpp" 22 | #include "../Starp.hpp" 23 | 24 | #include 25 | #include 26 | 27 | class AlgorithmComponent : public juce::Component, juce::Value::Listener { 28 | 29 | 30 | public: 31 | 32 | AlgorithmComponent(ProcessorParameters *params); 33 | 34 | void valueChanged(juce::Value &v) override; 35 | void paint(juce::Graphics&) override; 36 | void resized() override; 37 | 38 | private: 39 | 40 | ProcessorParameters *params_ = nullptr; 41 | 42 | AlgoChoiceComponent algoComponent_; 43 | RandomAlgoOptionsComponent randomComponent_; 44 | LinearAlgoOptionsComponent linearComponent_; 45 | SpiralAlgoOptionsComponent spiralComponent_; 46 | 47 | //========================================================== 48 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AlgorithmComponent) 49 | 50 | 51 | }; -------------------------------------------------------------------------------- /Source/EditorComponent/RandomAlgoOptionsComponent.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include "../AlgorithmParameters.hpp" 16 | 17 | #include 18 | #include 19 | 20 | #include 21 | 22 | using namespace solidfuel; 23 | 24 | 25 | using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment; 26 | 27 | class RandomAlgoOptionsComponent : public juce::Component { 28 | public : 29 | RandomAlgoOptionsComponent(RandomParameters * parms); 30 | 31 | void paint(juce::Graphics&) override; 32 | void resized() override; 33 | 34 | ~RandomAlgoOptionsComponent() override; 35 | 36 | private : 37 | 38 | RandomParameters *params_; 39 | juce::Value value_; 40 | juce::Value replace_; 41 | 42 | juce::Value seed_text_; 43 | 44 | juce::TextButton changeKeyButton_; 45 | juce::ToggleButton replaceButton_; 46 | 47 | juce::Label keyLabel_; 48 | juce::Label keyValueLabel_{"RandomKeyLabel", "00000000"}; 49 | 50 | ValueListener seed_listener_; 51 | ValueListener replace_listener_; 52 | 53 | void changeKey(); 54 | void update_seed_display(); 55 | void update_replace(); 56 | 57 | 58 | 59 | }; -------------------------------------------------------------------------------- /Source/EditorComponent/LinearAlgoOptionsComponent.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include "../AlgorithmParameters.hpp" 16 | 17 | #include 18 | #include 19 | 20 | #include 21 | 22 | using namespace solidfuel; 23 | 24 | 25 | class LinearAlgoOptionsComponent : public juce::Component { 26 | public : 27 | LinearAlgoOptionsComponent(LinearParameters * parms); 28 | 29 | void paint(juce::Graphics&) override; 30 | void resized() override; 31 | 32 | ~LinearAlgoOptionsComponent() override; 33 | 34 | 35 | private : 36 | 37 | LinearParameters *params_; 38 | 39 | // These two are a radio group 40 | juce::TextButton up_button_; 41 | juce::TextButton down_button_; 42 | 43 | juce::ToggleButton zigzag_button_; 44 | juce::ToggleButton restart_button_; 45 | 46 | ValueListener direction_listener_; 47 | ValueListener zigzag_listener_; 48 | ValueListener restart_listener_; 49 | 50 | void update_direction(); 51 | void update_zigzag(); 52 | void update_restart(); 53 | 54 | 55 | //========================================================== 56 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LinearAlgoOptionsComponent) 57 | 58 | }; -------------------------------------------------------------------------------- /Source/HashRandom.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #include "HashRandom.hpp" 14 | 15 | #include "TinySHA1.hpp" 16 | 17 | void HashRandom::initialize(const std::uint8_t digest[20]) { 18 | long long seed = digest[0] | 19 | ((long long)digest[1] << 8) | 20 | ((long long)digest[2] << 16) | 21 | ((long long)digest[3] << 24) | 22 | ((long long)digest[4] << 32) | 23 | ((long long)digest[5] << 40) | 24 | ((long long)digest[6] << 48) | 25 | ((long long)digest[7] << 56); 26 | 27 | rng_ = std::make_unique(seed); 28 | 29 | } 30 | 31 | 32 | HashRandom::HashRandom(const std::uint8_t digest[20]) { 33 | initialize(digest); 34 | } 35 | HashRandom::HashRandom(const std::string &category, long long key, double slot) { 36 | std::stringstream buf; 37 | buf << category << ':' << std::hex << key << ':' << slot; 38 | 39 | sha1::SHA1 hash; 40 | hash.processBytes(buf.str().c_str(), buf.str().size()); 41 | 42 | uint8_t digest[20]; 43 | hash.getDigestBytes(digest); 44 | initialize(digest); 45 | 46 | } 47 | int HashRandom::nextInt(int min, int max) { 48 | return rng_->nextInt({min, max}); 49 | } 50 | 51 | float HashRandom::nextFloat(float min, float max) { 52 | float range = max-min; 53 | float val= rng_->nextFloat(); 54 | 55 | return val*range + min; 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /Source/EditorComponent/SpiralAlgoOptionsComponent.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include "../AlgorithmParameters.hpp" 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | using namespace solidfuel; 22 | 23 | class SpiralAlgoOptionsComponent : public juce::Component { 24 | public : 25 | SpiralAlgoOptionsComponent(SpiralParameters * parms); 26 | 27 | void paint(juce::Graphics&) override; 28 | void resized() override; 29 | 30 | ~SpiralAlgoOptionsComponent() override; 31 | 32 | 33 | private : 34 | 35 | SpiralParameters *params_; 36 | 37 | // These two are a radio group 38 | juce::TextButton top_button_; 39 | juce::TextButton bottom_button_; 40 | 41 | // These 4 are a radio group 42 | 43 | juce::TextButton in_button_; 44 | juce::TextButton out_button_; 45 | juce::TextButton inout_button_; 46 | juce::TextButton outin_button_; 47 | 48 | ButtonGroupComponent direction_group_; 49 | 50 | ValueListener direction_listener_; 51 | ValueListener position_listener_; 52 | 53 | void update_direction(SpiralParameters::Direction direction); 54 | void update_position(); 55 | 56 | 57 | //========================================================== 58 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SpiralAlgoOptionsComponent) 59 | 60 | 61 | }; -------------------------------------------------------------------------------- /Source/ParamData.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #include "ParamData.hpp" 14 | 15 | //======================================================================== 16 | 17 | juce::Array AlgorithmIndexes {{ 18 | juce::var{Algorithm::Random}, 19 | juce::var{Algorithm::Linear}, 20 | juce::var{Algorithm::Spiral}, 21 | 22 | }}; 23 | 24 | juce::StringArray AlgorithmChoices{{ 25 | "Random", 26 | "Linear", 27 | "Spiral", 28 | }}; 29 | 30 | 31 | //======================================================================= 32 | juce::Array speed_parameter_values = { 33 | speed_value{"1/32" , 0.125}, 34 | speed_value{"1/16t" , 0.5/3.0 }, 35 | speed_value{"1/32d" , 0.1875 }, 36 | speed_value{"1/16q" , 0.20 }, 37 | speed_value{"1/16" , 0.25 }, 38 | speed_value{"1/8t" , 1.0/3.0 }, 39 | speed_value{"1/16d" , 0.375}, 40 | speed_value{"1/8q" , 0.40}, 41 | speed_value{"1/8" , 0.50}, 42 | speed_value{"1/4t" , 2.0/3.0 }, 43 | speed_value{"1/8d" , 0.75}, 44 | speed_value{"1/4q" , 0.8 }, 45 | speed_value{"1/4" , 1.0 }, 46 | speed_value{"1/2t" , 4.0/3.0 }, 47 | speed_value{"1/4d" , 1.5 }, 48 | speed_value{"1/2q" , 1.6 }, 49 | speed_value{"1/2" , 2.0 }, 50 | speed_value{"1/1t" , 8.0/3.0 }, 51 | speed_value{"1/2d" , 3.0 }, 52 | speed_value{"1/1q" , 3.2 }, 53 | speed_value{"1/1" , 4.0 }, 54 | }; 55 | 56 | 57 | juce::StringArray SpeedTypes{{ "note", "bar", "msec" }}; -------------------------------------------------------------------------------- /docs/INSTALLATION_GUIDE.md: -------------------------------------------------------------------------------- 1 | # solidArp Installation Guide 2 | 3 | The latest released version of _solidArp_ can be found on github at: 4 | 5 | https://github.com/SolidFuel/Arp/releases/latest 6 | 7 | There will be a few files available to help with install. 8 | Below are instructions for each plaform. 9 | 10 | ## Windows (64 bit) 11 | 12 | ### Installer 13 | The easiest way to install on windows is to use the included installer. 14 | Download the file `solidArp-VX.X.X-win64-install.exe`, where X.X.X will 15 | be the release number. 16 | 17 | **Note** Chrome browser will block the download with a warning about it 18 | not being "commonly downloaded". Click through 19 | the warning to download anyway. 20 | 21 | ### VST3 zip 22 | 23 | The zip file `solidArp-VX.X.X-win64-vst3.zip` contains the VST3 file. Unzip 24 | the archive and move the `solidArp.vst3` file to the `C:\Program Files\Common Files\VST3` 25 | directory. 26 | 27 | **Note** Chrome browser will block the download with a warning about it 28 | not being "commonly downloaded". Click through 29 | the warning to download anyway. 30 | 31 | ## Linux 32 | 33 | ### .deb package file 34 | 35 | The available `.deb` package is only for the `amd64` architecture. 36 | 37 | Download the `solidArp_X.X.X_linux-amd64.deb` file. Install using 38 | 39 | ``` 40 | sudo apt install ./solidArp_X.X.X_linux-amd64.deb 41 | ``` 42 | 43 | This will place the vst into the `/usr/lib/vst3` directory. 44 | 45 | ### VST3 zip 46 | 47 | _Note_ The release builds only work on x86_64 (amd64) architecture. 48 | 49 | Unzip the file `solidArp-Vx.x.x-linux-amd64.zip`. Place the resulting 50 | `solidArp.vst3` directory into `~/.vst3` 51 | 52 | This can also be placed in `/usr/local/lib/vst3` for system-wide use. You will 53 | need superuser privileges to do so. 54 | 55 | ## MacOS 56 | 57 | ### Installer 58 | A pkg style installer is provided that works for both intel and arm architectures. 59 | All version from 10.15 to 14.1 are supported. 60 | 61 | Download the file `solidArp_VX.X.X_macos-universal.pkg`. Double click on the file 62 | and follow the directions given. 63 | -------------------------------------------------------------------------------- /Source/AlgorithmParameters.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include 16 | 17 | #include "HashRandom.hpp" 18 | 19 | #include "Starp.hpp" 20 | 21 | struct RandomParameters { 22 | // Even with the L suffix it uses a 32 bit int. So 23 | // need to cast to int64 intentionally. 24 | juce::Value seed_value{juce::var{juce::int64{0L}}}; 25 | juce::int64 get_seed() const {return juce::int64(seed_value.getValue()); } 26 | 27 | juce::Value replace{juce::var{false}}; 28 | bool get_replace() const { return bool(replace.getValue()); } 29 | 30 | void pick_new_key() { 31 | juce::Random rng{}; 32 | seed_value = rng.nextInt64(); 33 | } 34 | }; 35 | 36 | struct LinearParameters { 37 | enum Direction { Up, Down }; 38 | 39 | juce::Value direction{Up}; 40 | int get_direction() const { return int(direction.getValue()); } 41 | 42 | juce::Value zigzag{juce::var{false}}; 43 | bool get_zigzag() const { return bool(zigzag.getValue()); } 44 | 45 | juce::Value restart{juce::var{false}}; 46 | bool get_restart() const { return bool(restart.getValue()); } 47 | }; 48 | 49 | struct SpiralParameters { 50 | enum StartPosition { Bottom, Top }; 51 | enum Direction { In, Out, InOut, OutIn }; 52 | 53 | juce::Value start_position{Top}; 54 | int get_start_position() const { return int(start_position.getValue()); } 55 | 56 | juce::Value direction{In}; 57 | int get_direction() const { return int(direction.getValue()); } 58 | }; 59 | -------------------------------------------------------------------------------- /.github/workflows/user-manual.yml: -------------------------------------------------------------------------------- 1 | name: User Manual to PDF 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | workflow_dispatch: 9 | 10 | # When pushing new commits, cancel any running builds on that branch 11 | concurrency: 12 | group: user-manual-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | BUILD_TYPE: Release 17 | BUILD_DIR: build 18 | OS_TAG : linux 19 | # Just to make vscode happy. These will be set for real 20 | # in the config-info step 21 | SF_PROJECT : "" 22 | SF_VERSION : "" 23 | SF_PROJ_LOWER : "" 24 | SF_ARTIFACT_PATH : "" 25 | SF_BUILD_FILE : "" 26 | SF_VST3_BUILD_PATH : "" 27 | SF_VST3_PLUGIN_PATH : "" 28 | SF_IN_RUNNER : "" 29 | 30 | 31 | jobs : 32 | manual : 33 | runs-on : ubuntu-latest 34 | 35 | steps: 36 | - name : install markdown-pdf 37 | run : sudo npm install -g markdown-pdf 38 | 39 | - name: Checkout Repo 40 | uses: actions/checkout@v3 41 | with: 42 | submodules : true 43 | 44 | - name: Get Config Info 45 | id: config_info 46 | shell : bash 47 | run : | 48 | mkdir build 49 | cd build 50 | ../common/scripts/project_vars.sh > CONFIG.sh 51 | cat CONFIG.sh 52 | cat CONFIG.sh >> $GITHUB_ENV 53 | cat CONFIG.sh >> $GITHUB_OUTPUT 54 | 55 | - name : convert 56 | working-directory: ${{github.workspace}}/docs 57 | run : | 58 | markdown-pdf -o ${{github.workspace}}/${{env.BUILD_DIR}}/USER_MANUAL.pdf USER_MANUAL.md 59 | markdown-pdf -o ${{github.workspace}}/${{env.BUILD_DIR}}/INSTALLATION_GUIDE.pdf INSTALLATION_GUIDE.md 60 | 61 | - name: Upload 62 | uses: actions/upload-artifact@v3 63 | with: 64 | name: ${{env.SF_PROJECT}}-${{env.SF_VERSION}}-MANUAL.pdf 65 | path: ${{github.workspace}}/${{env.BUILD_DIR}}/USER_MANUAL.pdf 66 | 67 | 68 | - name: Upload 69 | uses: actions/upload-artifact@v3 70 | with: 71 | name: ${{env.SF_PROJECT}}-INSTALLATION-GUIDE.pdf 72 | path: ${{github.workspace}}/build/INSTALLATION_GUIDE.pdf 73 | -------------------------------------------------------------------------------- /Source/ProcessorParameters.hpp: -------------------------------------------------------------------------------- 1 | 2 | /**** 3 | * solidArp - Stable Random Arpeggiator Plugin 4 | * Copyright (C) 2023 Solid Fuel 5 | * This program is free software: you can redistribute it and/or modify it 6 | * under the terms of the GNU General Public License as published by the 7 | * Free Software Foundation, either version 3 of the License, or (at your 8 | * option) any later version. This program is distributed in the hope that it 9 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 10 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 11 | * in the root directory. 12 | ****/ 13 | 14 | #pragma once 15 | 16 | #include "ParamData.hpp" 17 | #include "AlgorithmParameters.hpp" 18 | 19 | #include 20 | 21 | 22 | struct ProcessorParameters { 23 | 24 | static const juce::String SPEED_TYPE_ID; 25 | static const juce::String SPEED_NOTE_ID; 26 | static const juce::String SPEED_BAR_ID; 27 | static const juce::String SPEED_MSEC_ID; 28 | 29 | // These are not automatable. 30 | RandomParameters random_parameters; 31 | juce::int64 get_random_seed() const { return juce::int64(random_parameters.seed_value.getValue()); } 32 | LinearParameters linear_parameters; 33 | SpiralParameters spiral_parameters; 34 | 35 | juce::Value algorithm_index{juce::var{Algorithm::Random}}; 36 | int get_algo_index() const { return int(algorithm_index.getValue()); } 37 | 38 | // These are automatable and will live in the Value Tree 39 | juce::AudioParameterChoice* speed; 40 | juce::AudioParameterFloat* gate; 41 | juce::AudioParameterFloat* gate_range; 42 | juce::AudioParameterInt* velocity; 43 | juce::AudioParameterInt* velo_range; 44 | juce::AudioParameterInt* probability; 45 | juce::AudioParameterFloat* timing_delay; 46 | juce::AudioParameterFloat* timing_advance; 47 | juce::AudioParameterChoice* speed_type; 48 | juce::AudioParameterFloat* speed_bar; 49 | juce::AudioParameterFloat* speed_ms; 50 | 51 | std::unique_ptr apvts; 52 | 53 | ProcessorParameters(juce::AudioProcessor& processor); 54 | 55 | void pick_new_key(); 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /Source/EditorComponent/AlgoChoiceComponent.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #include "AlgoChoiceComponent.hpp" 14 | #include "../ParamData.hpp" 15 | #include "../Starp.hpp" 16 | 17 | 18 | AlgoChoiceComponent::AlgoChoiceComponent() : 19 | ChoicePropertyComponent("Algorithm") { 20 | 21 | DBGLOG("Creating Choice Component") 22 | choices.addArray(AlgorithmChoices); 23 | listener_.onChange = [this](juce::Value &){ refresh(); }; 24 | 25 | for (int i=0; i< AlgorithmIndexes.size(); ++i) { 26 | auto k = int(AlgorithmIndexes[i]); 27 | DBGLOG(" insert ", k, " => ", i) 28 | value_map_.insert({k, i}); 29 | } 30 | refresh(); 31 | DBGLOG("Creating Choice Component DONE") 32 | } 33 | 34 | void AlgoChoiceComponent::setValue(juce::Value &ptr) { 35 | DBGLOG("Setting value_ptr_"); 36 | value_ptr_.referTo(ptr); 37 | DBGLOG("Setting value_ptr_ adding Listener"); 38 | value_ptr_.addListener(&listener_); 39 | DBGLOG("Setting value_ptr_ DONE"); 40 | } 41 | 42 | void AlgoChoiceComponent::refresh() { 43 | DBGLOG("AlgoChoiceComponent::refresh called") 44 | juce::ChoicePropertyComponent::refresh(); 45 | } 46 | 47 | void AlgoChoiceComponent::addListener(juce::Value::Listener *l) { 48 | DBGLOG("AlgoChoiceComponent::addListener called") 49 | value_ptr_.addListener(l); 50 | } 51 | 52 | void AlgoChoiceComponent::setIndex(int newIndex) { 53 | DBGLOG("SETINDEX called = ", newIndex ); 54 | auto v = int(AlgorithmIndexes[newIndex]); 55 | DBGLOG(" => ", v); 56 | value_ptr_.setValue(v); 57 | } 58 | 59 | int AlgoChoiceComponent::getIndex() const { 60 | auto v = int(value_ptr_.getValue()); 61 | DBGLOG("GETINDEX called = ", v); 62 | auto i = value_map_.at(v); 63 | DBGLOG(" => ", i); 64 | return i; 65 | } 66 | -------------------------------------------------------------------------------- /Source/position_data.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include 16 | 17 | //============================================================================ 18 | struct position_data { 19 | 20 | // original position as given by the play head 21 | double qn_position = -1.0; 22 | 23 | double position_as_slots = -1.0; 24 | // slot_number is a whole number 25 | // slot_number + slot_fraction == position_as_slot 26 | double slot_fraction = 0.0; 27 | double slot_number = -1.0; 28 | 29 | int samples_per_qn; 30 | bool is_playing = false; 31 | 32 | // This represents how long the plugin has been active 33 | // in hires timer ticks. We use this to compute the stop 34 | // time for notes independent of the playhead. 35 | int64_t tick_count = 0; 36 | 37 | int64_t ticks_per_slot; 38 | 39 | // speed should be realtive to the quarter note. 40 | double speed = 1.0; 41 | 42 | // **NOTE** set speed before calling this function!! 43 | // 44 | void set_position(double new_position) { 45 | 46 | qn_position = new_position; 47 | 48 | position_as_slots = new_position / speed; 49 | 50 | slot_number = std::floor(position_as_slots); 51 | 52 | // how far along in the current slot are we ? 53 | slot_fraction = position_as_slots - slot_number; 54 | if (slot_fraction < 0.00001) { 55 | slot_fraction = 0.0; 56 | } else if (slot_fraction > 0.99999 ) { 57 | // floor(x) < x always. So, if x is whole number 58 | // we'll get the next lowest whole number. 59 | // This code tries to compensates for this. 60 | slot_fraction = 0.0; 61 | slot_number += 1; 62 | } 63 | 64 | 65 | 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /Source/EditorComponent/OverlayComponent.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include 16 | 17 | /* 18 | * A Component that manages several components that will overlay each other. 19 | * Only one child component will be shown at a time. Think of this as a 20 | * tabbed interface without the tabs actually showing. 21 | * 22 | * I suspect the template isn't actually needed and I could set ComponentType 23 | * to simply juce::Component and it would work. But this lets you check for problems 24 | * if you tighten the requirements. 25 | */ 26 | template class OverlayComponent : public juce::Component { 27 | 28 | juce::Array components; 29 | int active_index = -1; 30 | 31 | void setup_new_component(ComponentType * c) { 32 | components.add(c); 33 | if (active_index < 0 ) { 34 | c->setVisible(true); 35 | active_index = components.size()-1; 36 | } else { 37 | c->setVisible(false); 38 | } 39 | 40 | } 41 | 42 | public : 43 | OverlayComponent() {} 44 | 45 | void add(ComponentType * c) { 46 | setup_new_component(c); 47 | addChildComponent(c); 48 | } 49 | 50 | void add(ComponentType &c) { 51 | setup_new_component(&c); 52 | addChildComponent(c); 53 | } 54 | 55 | void setActiveIndex(int new_index) { 56 | if (new_index < components.size() && new_index >= 0 && new_index != active_index) { 57 | active_index = new_index; 58 | refresh(); 59 | } 60 | } 61 | 62 | void refresh() { 63 | 64 | for (int i = 0; i < components.size(); ++i ) { 65 | components[i]->setVisible(i == active_index); 66 | } 67 | 68 | resized(); 69 | } 70 | 71 | void resized() override { 72 | components[active_index]->setBounds(getLocalBounds()); 73 | } 74 | }; -------------------------------------------------------------------------------- /Source/PluginEditor.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #include "PluginProcessor.hpp" 14 | #include "PluginEditor.hpp" 15 | 16 | #include "Starp.hpp" 17 | 18 | 19 | constexpr int WIDTH = 620; 20 | constexpr int HEADER_HEIGHT = 50; 21 | constexpr int ALGORITHM_HEIGHT = 75; 22 | constexpr int PROPERTY_HEIGHT = 420; 23 | constexpr int HEIGHT = HEADER_HEIGHT + ALGORITHM_HEIGHT + PROPERTY_HEIGHT; 24 | constexpr int MARGIN = 5; 25 | 26 | //============================================================================== 27 | PluginEditor::PluginEditor (PluginProcessor& p) : 28 | AudioProcessorEditor (&p), //proc_ (p), 29 | algorithm_component_(p.getParameters()), 30 | property_component_(p.getParameters()) 31 | { 32 | 33 | addAndMakeVisible(header_component_); 34 | addAndMakeVisible(algorithm_component_); 35 | addAndMakeVisible(property_component_); 36 | 37 | setSize(WIDTH, HEIGHT); 38 | } 39 | 40 | PluginEditor::~PluginEditor() { 41 | 42 | } 43 | 44 | //============================================================================== 45 | void PluginEditor::paint (juce::Graphics& g) { 46 | // (Our component is opaque, so we must completely fill the background with a solid colour) 47 | g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId)); 48 | 49 | } 50 | 51 | void PluginEditor::resized() { 52 | 53 | using FlexItem = juce::FlexItem; 54 | juce::FlexBox box; 55 | 56 | box.flexDirection = juce::FlexBox::Direction::column; 57 | box.alignContent = juce::FlexBox::AlignContent::center; 58 | 59 | box.items.add(FlexItem(float(WIDTH), float(HEADER_HEIGHT), header_component_)); 60 | box.items.add(FlexItem(float(WIDTH), float(ALGORITHM_HEIGHT), algorithm_component_)); 61 | box.items.add(FlexItem(float(WIDTH-(MARGIN*2)), float(PROPERTY_HEIGHT), property_component_) 62 | .withMargin(FlexItem::Margin(0, MARGIN, 0, MARGIN))); 63 | 64 | 65 | box.performLayout (juce::Rectangle(0, 0, getWidth(), getHeight())); 66 | 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | 4 | #First, we'll add the CMake folder, in case we'll need to find_package later: 5 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMake") 6 | 7 | include("ReadConfig") 8 | 9 | message("SF_PROJECT = " ${SF_PROJECT}) 10 | message("SF_VERSION = " ${SF_VERSION}) 11 | 12 | option(UniversalBinary "Build universal binary for mac" ON) 13 | 14 | if (UniversalBinary) 15 | set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE INTERNAL "") 16 | endif() 17 | 18 | project("${SF_PROJECT}" VERSION "${SF_VERSION}") 19 | 20 | #Compile commands, useful for some IDEs like VS-Code 21 | set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) 22 | 23 | set(default_build_type "Release") 24 | if(NOT CMAKE_BUILD_TYPE) 25 | message(STATUS "Setting build type to '${default_build_type}' as none was specified.") 26 | set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE 27 | STRING "Choose the type of build." FORCE) 28 | else() 29 | message(STATUS "Build Type = ${CMAKE_BUILD_TYPE}") 30 | endif() 31 | 32 | #Minimum MacOS target, set globally 33 | 34 | if (${CMAKE_SYSTEM_NAME} STREQUAL "iOS") 35 | set(CMAKE_OSX_DEPLOYMENT_TARGET 11.0 CACHE STRING "Minimum OS X deployment version" FORCE) 36 | 37 | #code signing to run on an iOS device: 38 | # set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "iPhone Developer" CACHE STRING "" FORCE) 39 | # set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "XXXXXXXXXX" CACHE STRING "" FORCE) 40 | else() 41 | set(CMAKE_OSX_DEPLOYMENT_TARGET "10.11" CACHE STRING "Minimum OS X deployment version" FORCE) 42 | endif() 43 | 44 | #This is temporarily needed due to a bug in Xcode 15: 45 | if (CMAKE_CXX_COMPILER_ID MATCHES "AppleClang") 46 | if (NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS "15.0") 47 | add_compile_definitions(JUCE_SILENCE_XCODE_15_LINKER_WARNING=1) 48 | set(CMAKE_EXE_LINKER_FLAGS "-Wl,-ld_classic" CACHE INTERNAL "") 49 | endif () 50 | endif () 51 | 52 | if (CMAKE_SYSTEM_NAME STREQUAL "Windows") 53 | #static linking in Windows 54 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 55 | add_compile_options("$<$:/O2>") 56 | endif() 57 | 58 | add_compile_definitions("$<$:SF_DEBUG=1>") 59 | 60 | option(NANCHECK "Build NaN checking" OFF) 61 | 62 | if (NANCHECK) 63 | message(STATUS "Compiling in NANCHECK" ) 64 | add_compile_definitions("NANCHECK=1") 65 | endif() 66 | 67 | #We 'require' that we need juce. If JUCE isn't found, it will revert to what's in 68 | #CMake/Findjuce.cmake, where you can see how JUCE is brought in/configured 69 | find_package(juce REQUIRED) 70 | 71 | add_subdirectory(external) 72 | 73 | add_subdirectory(common) 74 | 75 | add_subdirectory(Source) 76 | 77 | #optionally, we're also adding the unit tests: 78 | option(BUILD_UNIT_TESTS "Build unit tests" ON) 79 | 80 | if (BUILD_UNIT_TESTS) 81 | enable_testing() 82 | add_subdirectory(Tests) 83 | endif() 84 | -------------------------------------------------------------------------------- /.github/workflows/linux-release.yml: -------------------------------------------------------------------------------- 1 | name: Linux Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | workflow_dispatch: 9 | 10 | # When pushing new commits, cancel any running builds on that branch 11 | concurrency: 12 | group: linux-build-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | BUILD_TYPE: Release 17 | BUILD_DIR: build 18 | OS_TAG : linux 19 | # Just to make vscode happy. These will be set for real 20 | # in the config-info step 21 | SF_PROJECT : "" 22 | SF_VERSION : "" 23 | SF_PROJ_LOWER : "" 24 | SF_ARTIFACT_PATH : "" 25 | SF_BUILD_FILE : "" 26 | SF_VST3_BUILD_PATH : "" 27 | SF_VST3_PLUGIN_PATH : "" 28 | SF_OUTPUT_STEM : "" 29 | SF_IN_RUNNER: "" 30 | 31 | 32 | jobs: 33 | build: 34 | runs-on: 'ubuntu-latest' 35 | steps: 36 | - name: Configure ubuntu 37 | run : | 38 | sudo apt-get update 39 | sudo apt install xorg-dev libasound2-dev libgl1-mesa-dev \ 40 | libcurl4-openssl-dev libwebkit2gtk-4.0-dev 41 | 42 | - name: Checkout Repo 43 | uses: actions/checkout@v3 44 | with: 45 | submodules: recursive 46 | 47 | - name: Get Config Info 48 | id: config-info 49 | shell : bash 50 | run : | 51 | ./common/scripts/project_vars.sh > CONFIG.sh 52 | cat CONFIG.sh 53 | cat CONFIG.sh >> $GITHUB_ENV 54 | cat CONFIG.sh >> $GITHUB_OUTPUT 55 | 56 | - name: Build 57 | uses : SolidFuel/actions/config-build-test@main 58 | with : 59 | build_dir: ${{env.BUILD_DIR}} 60 | type : ${{env.BUILD_TYPE}} 61 | 62 | - name: Pluginval 63 | uses : SolidFuel/actions/test-with-pluginval@main 64 | with : 65 | build_dir: ${{env.BUILD_DIR}} 66 | plugin : ${{env.SF_VST3_PLUGIN_PATH}} 67 | 68 | - name: Compress plugin 69 | id : mkzip 70 | working-directory: ${{github.workspace}}/build 71 | run : | 72 | cd ${{env.SF_VST3_BUILD_PATH}}/${{env.SF_ARTIFACT_PATH}} 73 | ZIPNAME="${{env.SF_OUTPUT_STEM}}-vst3.zip" 74 | zip -9 -r ${{github.workspace}}/build/${ZIPNAME} ${{env.SF_BUILD_FILE}} 75 | echo "ZIPNAME=$ZIPNAME" >> "$GITHUB_OUTPUT" 76 | 77 | - name: Upload plugin 78 | uses: actions/upload-artifact@v3 79 | with: 80 | name: ${{steps.mkzip.outputs.ZIPNAME}} 81 | path: ${{github.workspace}}/build/${{steps.mkzip.outputs.ZIPNAME}} 82 | 83 | 84 | - name: Build .deb 85 | id: build_deb 86 | working-directory: ${{github.workspace}}/build 87 | run : ../common/scripts/create-deb.sh 88 | 89 | - name: Upload deb 90 | uses: actions/upload-artifact@v3 91 | with: 92 | name: ${{steps.build_deb.outputs.deb_file}} 93 | path: ${{github.workspace}}/build/${{steps.build_deb.outputs.deb_file}} 94 | 95 | -------------------------------------------------------------------------------- /.github/workflows/win64-release.yml: -------------------------------------------------------------------------------- 1 | name: Windows Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | workflow_dispatch: 9 | 10 | # When pushing new commits, cancel any running builds on that branch 11 | concurrency: 12 | group: windows-build-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | BUILD_TYPE: Release 17 | BUILD_DIR: build 18 | OS_TAG : win64 19 | # Just to make vscode happy. These will be set for real 20 | # in the config-info step 21 | SF_PROJECT : "" 22 | SF_VERSION : "" 23 | SF_PROJ_LOWER : "" 24 | SF_ARTIFACT_PATH : "" 25 | SF_BUILD_FILE : "" 26 | SF_VST3_BUILD_PATH : "" 27 | SF_VST3_PLUGIN_PATH : "" 28 | SF_OUTPUT_STEM : "" 29 | SF_IN_RUNNER : "" 30 | 31 | jobs: 32 | build: 33 | runs-on: 'windows-latest' 34 | steps: 35 | - name: Checkout Repo 36 | uses: actions/checkout@v3 37 | with: 38 | submodules: true 39 | 40 | - name: Get Config Info 41 | id: config-info 42 | shell : bash 43 | run : | 44 | ./common/scripts/project_vars.sh > CONFIG.sh 45 | cat CONFIG.sh 46 | cat CONFIG.sh >> $GITHUB_ENV 47 | cat CONFIG.sh >> $GITHUB_OUTPUT 48 | 49 | - name: Build 50 | uses : SolidFuel/actions/config-build-test@main 51 | with : 52 | build_dir: ${{env.BUILD_DIR}} 53 | type : ${{env.BUILD_TYPE}} 54 | 55 | - name: Pluginval 56 | uses : SolidFuel/actions/test-with-pluginval@main 57 | with : 58 | build_dir: ${{env.BUILD_DIR}} 59 | plugin : ${{env.SF_VST3_PLUGIN_PATH}} 60 | 61 | - name: Compress plugin win 62 | working-directory: ${{github.workspace}}/${{env.BUILD_DIR}} 63 | run : | 64 | Set-Location ${env:SF_VST3_BUILD_PATH}/${env:SF_ARTIFACT_PATH} 65 | $ZIPNAME="${env:SF_OUTPUT_STEM}-vst3.zip" 66 | Compress-Archive -Path ${env:SF_BUILD_FILE} ` 67 | -CompressionLevel Optimal ` 68 | -Destination ${ZIPNAME} 69 | 70 | - name: Upload plugin 71 | uses: actions/upload-artifact@v3 72 | with: 73 | name: ${{env.SF_OUTPUT_STEM}}-vst3.zip 74 | path: ${{github.workspace}}/${{env.BUILD_DIR}}/${{env.SF_OUTPUT_STEM}}-vst3.zip 75 | 76 | - name : setup windows installer file 77 | working-directory: ${{github.workspace}} 78 | shell : bash 79 | run : ./common/scripts/expand_file.sh ./packaging/windows/plugin.iss > ${{env.BUILD_DIR}}/plugin.iss 80 | 81 | - name : build windows installer 82 | working-directory: ${{github.workspace}} 83 | run: iscc /Dsrcdir="$pwd" ${{env.BUILD_DIR}}\plugin.iss 84 | 85 | - name: Upload windows installer 86 | uses: actions/upload-artifact@v3 87 | with: 88 | name: ${{env.SF_OUTPUT_STEM}}-install.exe 89 | path: ${{github.workspace}}/${{env.BUILD_DIR}}/${{env.SF_PROJ_LOWER}}.exe 90 | -------------------------------------------------------------------------------- /Source/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | set (BaseTargetName "${SF_PROJECT}") 3 | 4 | add_compile_definitions("$<$:SF_DEBUG=0>") 5 | 6 | include(GetGitRevisionDescription) 7 | 8 | get_git_head_revision(GIT_REF GIT_HASH) 9 | 10 | string(SUBSTRING "${GIT_HASH}" 0 7 GIT_SHORT_HASH ) 11 | 12 | message("GIT_HASH = " ${GIT_HASH}) 13 | message("GIT_SHORT_HASH = " ${GIT_SHORT_HASH}) 14 | message("GIT_REF = " ${GIT_REF}) 15 | 16 | include_directories(${CMAKE_CURRENT_BINARY_DIR}) 17 | 18 | configure_file(version.hpp.in version.hpp @ONLY) 19 | 20 | string(TOUPPER "${CMAKE_BUILD_TYPE}" upper_build_type) 21 | if(${upper_build_type} STREQUAL "RELEASE") 22 | set(PLUGIN_PRODUCT_NAME "${SF_PROJECT}") 23 | set(PLUGIN_PRODUCT_CODE "Sfar" ) 24 | else() 25 | set(PLUGIN_PRODUCT_NAME "${SF_PROJECT}Dev") 26 | set(PLUGIN_PRODUCT_CODE "SfaD") 27 | endif() 28 | 29 | string(TOUPPER "${PLUGIN_PRODUCT_NAME}" PLUGIN_NAME_UPPER) 30 | 31 | configure_file(version.hpp.in version.hpp @ONLY) 32 | 33 | juce_add_plugin("${BaseTargetName}" 34 | # VERSION ... # Set this if the plugin version is different to the project version 35 | # ICON_BIG ... # ICON_* arguments specify a path to an image file to use as an icon for the Standalone 36 | # ICON_SMALL ... 37 | COMPANY_NAME "SolidFuel" 38 | IS_SYNTH TRUE 39 | NEEDS_MIDI_INPUT TRUE 40 | NEEDS_MIDI_OUTPUT TRUE 41 | IS_MIDI_EFFECT FALSE 42 | EDITOR_WANTS_KEYBOARD_FOCUS FALSE 43 | COPY_PLUGIN_AFTER_BUILD FALSE 44 | PLUGIN_MANUFACTURER_CODE Sdfl 45 | PLUGIN_CODE ${PLUGIN_PRODUCT_CODE} 46 | FORMATS VST3 47 | PRODUCT_NAME ${PLUGIN_PRODUCT_NAME} ) 48 | 49 | target_sources(${BaseTargetName} PRIVATE 50 | HashRandom.hpp HashRandom.cpp 51 | position_data.hpp 52 | Algorithms/AlgoBase.hpp 53 | Algorithms/RandomAlgorithm.hpp 54 | Algorithms/LinearAlgorithm.hpp 55 | Algorithms/SpiralAlgorithm.hpp 56 | AlgorithmEnum.hpp AlgorithmParameters.hpp Algorithm.hpp 57 | ParamData.hpp ParamData.cpp 58 | PluginEditor.hpp PluginEditor.cpp 59 | EditorComponent/OverlayComponent.hpp 60 | EditorComponent/AlgoChoiceComponent.hpp 61 | EditorComponent/AlgoChoiceComponent.cpp 62 | EditorComponent/RandomAlgoOptionsComponent.hpp 63 | EditorComponent/RandomAlgoOptionsComponent.cpp 64 | EditorComponent/LinearAlgoOptionsComponent.hpp 65 | EditorComponent/LinearAlgoOptionsComponent.cpp 66 | EditorComponent/SpiralAlgoOptionsComponent.hpp 67 | EditorComponent/SpiralAlgoOptionsComponent.cpp 68 | EditorComponent/AlgorithmComponent.hpp 69 | EditorComponent/AlgorithmComponent.cpp 70 | EditorComponent/PropertyComponent.hpp 71 | EditorComponent/PropertyComponent.cpp 72 | EditorComponent/HeaderComponent.hpp 73 | EditorComponent/HeaderComponent.cpp 74 | ProcessorParameters.hpp ProcessorParameters.cpp 75 | PluginProcessor.hpp 76 | Processor/processing.cpp 77 | Processor/setup.cpp 78 | ) 79 | 80 | target_compile_definitions(${BaseTargetName} 81 | PUBLIC 82 | JUCE_WEB_BROWSER=0 83 | JUCE_USE_CURL=0 84 | JUCE_DISPLAY_SPLASH_SCREEN=0 85 | JUCE_VST3_CAN_REPLACE_VST2=0) 86 | 87 | target_link_libraries(${BaseTargetName} PRIVATE 88 | TinySHA1 89 | solidfuel 90 | juce_recommended_config_flags 91 | juce_recommended_lto_flags 92 | juce_recommended_warning_flags) 93 | -------------------------------------------------------------------------------- /Source/Algorithms/RandomAlgorithm.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include "AlgoBase.hpp" 16 | #include "../AlgorithmParameters.hpp" 17 | 18 | #include "../HashRandom.hpp" 19 | #include "../Starp.hpp" 20 | 21 | #include 22 | 23 | 24 | 25 | class RandomAlgorithm : public AlgorithmBase { 26 | 27 | // Non owning 28 | RandomParameters *p_; 29 | 30 | juce::int64 seed_; 31 | bool replace_; 32 | 33 | juce::SortedSet available_notes; 34 | 35 | int last_note = -1; 36 | 37 | 38 | public: 39 | 40 | RandomAlgorithm(RandomParameters *p) : p_(p) { 41 | 42 | seed_listener_.onChange = [this](juce::Value &) { 43 | update_parameters(); 44 | }; 45 | p_->seed_value.addListener(&seed_listener_); 46 | 47 | replace_listener_.onChange = [this](juce::Value &) { 48 | update_parameters(); 49 | }; 50 | p_->replace.addListener(&replace_listener_); 51 | 52 | update_parameters(); 53 | } 54 | 55 | void reset() override { 56 | AlgorithmBase::reset(); 57 | last_note = -1; 58 | available_notes.clearQuick(); 59 | } 60 | 61 | Algorithm get_algo() const override { return Algorithm:: Random; } 62 | 63 | 64 | int getNextNote(double timeline_slot, const juce::SortedSet ¬es, bool notes_changed) override { 65 | 66 | DBGLOG("Random GETNEXTNOTE called slot = ", int(timeline_slot), " changed = ", notes_changed) 67 | 68 | if (notes_changed || available_notes.isEmpty()) { 69 | available_notes.clearQuick(); 70 | available_notes.addSet(notes); 71 | } 72 | 73 | int num_notes = available_notes.size(); 74 | DBGLOG("available note count = ", num_notes) 75 | 76 | if (num_notes == 0) { 77 | return -1; 78 | } 79 | 80 | if ( num_notes == 1 ) { 81 | last_note = available_notes[0]; 82 | available_notes.clear(); 83 | return last_note; 84 | } 85 | 86 | last_note = getRandom(timeline_slot, last_note, available_notes); 87 | if (!replace_) { 88 | available_notes.removeValue(last_note); 89 | } 90 | return last_note; 91 | } 92 | 93 | ~RandomAlgorithm() override { 94 | // We don't own the p_, so it might out live us. 95 | // explicitly remove the listener. 96 | p_->seed_value.removeListener(&seed_listener_); 97 | p_->replace.removeListener(&replace_listener_); 98 | } 99 | 100 | private : 101 | 102 | solidfuel::ValueListener seed_listener_; 103 | solidfuel::ValueListener replace_listener_; 104 | 105 | void update_parameters() { 106 | DBGLOG("RandomAlgorithm::update_parameters called") 107 | seed_ = p_->get_seed(); 108 | DBGLOG("RandomAlgorithm::update_parameters seed done") 109 | replace_ = p_->get_replace(); 110 | DBGLOG("RandomAlgorithm::update_parameters replace done") 111 | } 112 | 113 | 114 | int getRandom(double slot, int note_to_avoid, const juce::SortedSet & notes) { 115 | 116 | 117 | HashRandom rng{"Note", seed_, slot}; 118 | 119 | for (int j=0; j < 20; ++j) { 120 | // need to do better, but this is an okay proof of concept 121 | int maybe = notes[rng.nextInt(0, notes.size())]; 122 | if (maybe != note_to_avoid) 123 | return maybe; 124 | } 125 | 126 | return -1; 127 | } 128 | 129 | }; 130 | 131 | 132 | -------------------------------------------------------------------------------- /Source/EditorComponent/AlgorithmComponent.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #include "AlgorithmComponent.hpp" 14 | #include "../Starp.hpp" 15 | 16 | AlgorithmComponent::AlgorithmComponent (ProcessorParameters *params) : params_(params), 17 | randomComponent_(¶ms->random_parameters), 18 | linearComponent_(¶ms->linear_parameters), 19 | spiralComponent_(¶ms->spiral_parameters) { 20 | 21 | DBGLOG("AlgorithmComponent constructor called"); 22 | 23 | 24 | algoComponent_.setValue(params->algorithm_index); 25 | addAndMakeVisible(algoComponent_); 26 | algoComponent_.refresh(); 27 | algoComponent_.addListener(this); 28 | 29 | addChildComponent(randomComponent_); 30 | addChildComponent(linearComponent_); 31 | addChildComponent(spiralComponent_); 32 | 33 | DBGLOG("Setting up AlgorithmComponent CHECK 1"); 34 | 35 | // Make sure we are in synch with the current value 36 | valueChanged(params->algorithm_index); 37 | 38 | } 39 | 40 | void AlgorithmComponent::valueChanged(juce::Value &) { 41 | auto algo = params_->get_algo_index(); 42 | 43 | DBGLOG("AlgorithmComponent::valueChanged = ", algo) 44 | 45 | randomComponent_.setVisible(algo == Algorithm::Random); 46 | linearComponent_.setVisible(algo == Algorithm::Linear); 47 | spiralComponent_.setVisible(algo == Algorithm::Spiral); 48 | 49 | resized(); 50 | 51 | } 52 | 53 | //============================================================================== 54 | void AlgorithmComponent::paint (juce::Graphics& g) { 55 | // (Our component is opaque, so we must completely fill the background with a solid colour) 56 | g.fillAll (getLookAndFeel().findColour (juce::PropertyComponent::backgroundColourId)); 57 | 58 | } 59 | 60 | 61 | //============================================================================== 62 | void AlgorithmComponent::resized() { 63 | 64 | DBGLOG("AlgorithmComponent::resized called") 65 | 66 | juce::Grid grid; 67 | 68 | using Track = juce::Grid::TrackInfo; 69 | using Fr = juce::Grid::Fr; 70 | using GridItem = juce::GridItem; 71 | 72 | grid.alignItems = juce::Grid::AlignItems::center; 73 | grid.justifyContent = juce::Grid::JustifyContent::start; 74 | grid.justifyItems = juce::Grid::JustifyItems::start; 75 | grid.templateColumns = { Track (Fr (1)) }; 76 | 77 | //---------------------------------------- 78 | // Algorithm Choice 79 | grid.templateRows.add(Track (Fr (1))); 80 | grid.items.add(GridItem(algoComponent_).withMargin({5, 30, 5, 30})); 81 | 82 | DBGLOG("AlgorithmComponent Algorithm Choice DONE"); 83 | 84 | //---------------------------------------- 85 | // Algorithm Specific Options 86 | 87 | auto algo = params_->get_algo_index(); 88 | DBGLOG("algo = ", algo) 89 | juce::Component *optionComponent = nullptr; 90 | switch (algo) { 91 | case Algorithm::Random : 92 | optionComponent = &randomComponent_; 93 | break; 94 | case Algorithm::Linear : 95 | optionComponent = &linearComponent_; 96 | break; 97 | case Algorithm::Spiral : 98 | optionComponent = &spiralComponent_; 99 | break; 100 | default : 101 | jassertfalse; 102 | } 103 | 104 | jassert(optionComponent != nullptr); 105 | 106 | grid.templateRows.add(Track (Fr (1))); 107 | grid.items.add(GridItem(*optionComponent).withMargin({0, 5, 0, 5})); 108 | 109 | DBGLOG("AlgorithmComponent Algorithm Options DONE"); 110 | 111 | grid.performLayout (getLocalBounds()); 112 | } 113 | 114 | -------------------------------------------------------------------------------- /project_vars.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Calculate the project variables and place shell lines 4 | # on stdout to define them 5 | # 6 | # project_vars -cfg -exp 7 | # 8 | # config file : (optional) Path and name of the PLUGIN_CONFIG file to use. 9 | # If this is not given (or is empty), it will try to find it by looking 10 | # up the directory heirarchy 11 | # 12 | # exports : (optional) If present, the script will generate export statements 13 | # as well. 14 | 15 | cfg_file="" 16 | do_export="" 17 | 18 | #set -x 19 | 20 | while [[ "$1" ]]; do 21 | case "$1" in 22 | -cfg) 23 | cfg_file="$2" 24 | shift; shift 25 | ;; 26 | -exp) 27 | do_export=$2 28 | shift; shift 29 | ;; 30 | *) 31 | echo "ERROR: Unknown option '$1'" 32 | exit 1 33 | esac 34 | done 35 | 36 | if [[ $do_export == "" || $do_export == "ps" || $do_export == "sh" ]]; then 37 | true 38 | else 39 | echo "ERROR: Invalid value for -exp : '$do_export'" 40 | exit 1 41 | fi 42 | 43 | # Find the config file if they didn't give it to us. 44 | if [[ -z "$cfg_file" || "X${cfg_file}Z" == "X-Z" ]]; then 45 | cfg_file="PLUGIN_CONFIG" 46 | for file in "${cfg_file}" "../${cfg_file}" 47 | do 48 | if [ -e "$file" ]; then 49 | cfg_file=$file 50 | break 51 | fi 52 | done 53 | 54 | if [ ! -f "${cfg_file}" ]; then 55 | echo "ERROR: Could not find CONFIG file" 56 | exit 1 57 | fi 58 | elif [ ! -f "$cfg_file" ]; then 59 | echo "ERROR: file '${cfg_file}' does not exist." 60 | exit 1 61 | fi 62 | 63 | if [ "$do_export" == "ps" ]; then 64 | export="\$env:" 65 | elif [[ $do_export == "sh" ]]; then 66 | export="export " 67 | else 68 | export="" 69 | fi 70 | 71 | if [ -z "$OS_TAG" ]; then 72 | uname=`uname` 73 | case ${uname} in 74 | "Darwin") 75 | OS_TAG="macos" 76 | ;; 77 | "Linux") 78 | OS_TAG="linux" 79 | ;; 80 | *) 81 | echo "Cannot determine the OS from '${uname}'" 82 | exit 1 83 | ;; 84 | esac 85 | fi 86 | 87 | Q="" 88 | if [[ "$do_export" == "ps" ]]; then 89 | # powershell values need to surrounded in quotes 90 | Q='"' 91 | fi 92 | 93 | SEP="/" 94 | if [[ "$OS_TAG" == "win64" ]]; then 95 | SEP="\\" 96 | fi 97 | 98 | # Output Variables from the config file 99 | echo "${export}OS_TAG=$Q${OS_TAG}$Q"; 100 | while read -r line 101 | do 102 | if [ $line == "#*" ]; then 103 | continue 104 | fi 105 | if [[ "$do_export" == "ps" ]]; then 106 | IFS="=" read -r key value <<< "$line" 107 | echo "${export}${key}=\"$value\"" 108 | else 109 | echo "${export}${line}" 110 | fi 111 | done < ${cfg_file} 112 | 113 | # --- Create derived variables 114 | # 115 | source "${cfg_file}" 116 | 117 | PROJ_LOWER=$(echo "$SF_PROJECT" | tr '[:upper:]' '[:lower:]') 118 | 119 | OUTPUT_STEM=${SF_PROJECT}-V${SF_VERSION}-${OS_TAG} 120 | 121 | case "$OS_TAG" in 122 | "macos") 123 | ARTIFACT_PATH="." 124 | BUILD_FILE=${SF_PROJECT}.vst3 125 | SEP="/" 126 | ;; 127 | "linux") 128 | ARTIFACT_PATH="." 129 | BUILD_FILE=${SF_PROJECT}.vst3 130 | SEP="/" 131 | ;; 132 | "win64") 133 | ARTIFACT_PATH="${SF_PROJECT}.vst3\\Contents\\x86_64-win" 134 | BUILD_FILE=${SF_PROJECT}.vst3 135 | SEP="\\" 136 | ;; 137 | *) 138 | echo "ERROR: unknown OS_TAG = '$OS_TAG'" 139 | exit 1 140 | ;; 141 | esac 142 | 143 | IN_RUNNER="" 144 | if [[ -n "$GITHUB_OUTPUT" ]]; then 145 | IN_RUNNER=1 146 | fi 147 | 148 | VST3_BUILD_PATH="Source${SEP}${SF_PROJECT}_artefacts${SEP}Release${SEP}VST3" 149 | 150 | # OUTPUT derived variables 151 | echo "${export}SF_PROJ_LOWER=$Q${PROJ_LOWER}$Q"; 152 | echo "${export}SF_ARTIFACT_PATH=$Q${ARTIFACT_PATH}$Q"; 153 | echo "${export}SF_BUILD_FILE=$Q${BUILD_FILE}$Q"; 154 | echo "${export}SF_VST3_BUILD_PATH=$Q${VST3_BUILD_PATH}$Q"; 155 | echo "${export}SF_VST3_PLUGIN_PATH=$Q${VST3_BUILD_PATH}${SEP}${ARTIFACT_PATH}${SEP}${BUILD_FILE}$Q"; 156 | echo "${export}SF_OUTPUT_STEM=$Q${OUTPUT_STEM}$Q"; 157 | echo "${export}SF_IN_RUNNER=$Q${IN_RUNNER}$Q"; 158 | -------------------------------------------------------------------------------- /.github/workflows/macos-release.yml: -------------------------------------------------------------------------------- 1 | name: MacOS Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | workflow_dispatch: 9 | 10 | # When pushing new commits, cancel any running builds on that branch 11 | concurrency: 12 | group: macos-build-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | BUILD_TYPE: Release 17 | BUILD_DIR: build 18 | OS_TAG : macos 19 | # Just to make vscode happy. These will be set for real 20 | # in the config-info step 21 | SF_PROJECT : "" 22 | SF_VERSION : "" 23 | SF_PROJ_LOWER : "" 24 | SF_VST3_BUILD_PATH : "" 25 | SF_ARTIFACT_PATH : "" 26 | SF_BUILD_FILE : "" 27 | SF_VST3_PLUGIN_PATH : "" 28 | SF_OUTPUT_STEM: "" 29 | SF_IN_RUNNER: "" 30 | 31 | 32 | jobs: 33 | build: 34 | runs-on: 'macos-latest' 35 | steps: 36 | - name: Checkout Repo 37 | uses: actions/checkout@v3 38 | with: 39 | submodules: recursive 40 | 41 | - name: Get Config Info 42 | id: config-info 43 | shell : bash 44 | run : | 45 | ./common/scripts/project_vars.sh > CONFIG.sh 46 | cat CONFIG.sh 47 | cat CONFIG.sh >> "$GITHUB_ENV" 48 | cat CONFIG.sh >> "$GITHUB_OUTPUT" 49 | 50 | - name: Build 51 | uses : SolidFuel/actions/config-build-test@main 52 | with : 53 | build_dir: ${{env.BUILD_DIR}} 54 | type : ${{env.BUILD_TYPE}} 55 | 56 | - name: Pluginval 57 | uses : SolidFuel/actions/test-with-pluginval@main 58 | with : 59 | build_dir: ${{env.BUILD_DIR}} 60 | plugin : ${{env.SF_VST3_PLUGIN_PATH}} 61 | 62 | - name: Import APP Certificates 63 | uses: apple-actions/import-codesign-certs@v2 64 | with: 65 | keychain-password : ${{ secrets.SIGN_KEYCHAIN_PASSWORD }} 66 | p12-file-base64: ${{ secrets.SIGN_APP_CERT }} 67 | p12-password: ${{ secrets.SIGN_APP_PASSWORD }} 68 | 69 | - name: Import INST Certificates 70 | uses: apple-actions/import-codesign-certs@v2 71 | with: 72 | keychain-password : ${{ secrets.SIGN_KEYCHAIN_PASSWORD }} 73 | create-keychain : false 74 | p12-file-base64: ${{ secrets.SIGN_INSTALL_CERT }} 75 | p12-password: ${{ secrets.SIGN_INSTALL_PASSWORD }} 76 | 77 | - name : Codesign plugin 78 | working-directory : ${{github.workspace}}/build 79 | run : | 80 | BINARY="${{env.SF_VST3_PLUGIN_PATH}}/Contents/MacOS/${{env.SF_PROJECT}}" 81 | codesign --force -s "${{ secrets.DEVELOPER_ID_APPLICATION}}" \ 82 | -v "${BINARY}" \ 83 | --strict --options=runtime --timestamp 84 | 85 | codesign --force -s "${{ secrets.DEVELOPER_ID_APPLICATION}}" \ 86 | -v "${{env.SF_VST3_PLUGIN_PATH}}" \ 87 | --strict --options=runtime --timestamp 88 | 89 | - name : Create PKG file 90 | id : create_pkg 91 | working-directory : ${{github.workspace}}/build 92 | run : | 93 | pkgbuild --component ${{env.SF_VST3_PLUGIN_PATH}} \ 94 | --install-location /Library/Audio/Plug-Ins/VST3/SolidFuel \ 95 | --sign "${{secrets.DEVELOPER_ID_INSTALLER}}" \ 96 | ./${{env.SF_PROJECT}}-unsigned.pkg 97 | 98 | productbuild --package ./${{env.SF_PROJECT}}-unsigned.pkg \ 99 | --sign "${{secrets.DEVELOPER_ID_INSTALLER}}" \ 100 | ./${{env.SF_OUTPUT_STEM}}-universal.pkg 101 | 102 | echo "PKGNAME=${{env.SF_OUTPUT_STEM}}-universal.pkg" >> "$GITHUB_OUTPUT" 103 | 104 | 105 | - name : Notarize PKG 106 | working-directory : ${{github.workspace}}/build 107 | run : | 108 | echo "---Notarize---" 109 | xcrun notarytool submit "${{steps.create_pkg.outputs.PKGNAME }}" \ 110 | --apple-id "${{ secrets.NOTARIZATION_USERNAME }}" \ 111 | --password "${{ secrets.NOTARIZATION_PASSWORD }}" \ 112 | --team-id "${{ secrets.TEAM_ID }}" --wait 113 | echo "---Notarized---" 114 | 115 | echo "---Staple---" 116 | xcrun stapler staple "${{steps.create_pkg.outputs.PKGNAME }}" 117 | echo "---Stapled---" 118 | 119 | - name: Upload plugin 120 | uses: actions/upload-artifact@v3 121 | with: 122 | name: ${{steps.create_pkg.outputs.PKGNAME }} 123 | path: ${{github.workspace}}/${{env.BUILD_DIR}}/${{steps.create_pkg.outputs.PKGNAME }} 124 | 125 | -------------------------------------------------------------------------------- /Source/Algorithms/LinearAlgorithm.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include "AlgoBase.hpp" 16 | #include "../AlgorithmParameters.hpp" 17 | 18 | #include "../Starp.hpp" 19 | 20 | #include 21 | 22 | using namespace solidfuel; 23 | 24 | 25 | class LinearAlgorithm : public AlgorithmBase { 26 | 27 | private: 28 | LinearParameters * p_; 29 | 30 | int direction = LinearParameters::Direction::Up; 31 | bool zigzag = false; 32 | bool restart = false; 33 | 34 | int clock = -1; 35 | 36 | int last_slot = 0; 37 | 38 | 39 | ValueListener direction_listener_; 40 | ValueListener zigzag_listener_; 41 | ValueListener restart_listener_; 42 | 43 | void update_parameters() { 44 | DBGLOG("LinearAlgorithm::update_parameters called") 45 | direction = p_->get_direction(); 46 | zigzag = p_->get_zigzag(); 47 | restart = p_->get_restart(); 48 | } 49 | 50 | public : 51 | LinearAlgorithm(LinearParameters * p) : p_{p} { 52 | update_parameters(); 53 | 54 | direction_listener_.onChange = [this](juce::Value &) { 55 | update_parameters(); 56 | }; 57 | 58 | p_->direction.addListener(&direction_listener_); 59 | 60 | zigzag_listener_.onChange = [this](juce::Value &) { 61 | update_parameters(); 62 | }; 63 | 64 | p_->zigzag.addListener(&zigzag_listener_); 65 | 66 | restart_listener_.onChange = [this](juce::Value &) { 67 | update_parameters(); 68 | }; 69 | 70 | p_->restart.addListener(&restart_listener_); 71 | } 72 | 73 | ~LinearAlgorithm() override { 74 | p_->direction.removeListener(&direction_listener_); 75 | p_->zigzag.removeListener(&zigzag_listener_); 76 | p_->restart.removeListener(&restart_listener_); 77 | } 78 | 79 | Algorithm get_algo() const override { return Algorithm::Linear; } 80 | 81 | int getNextNote(double slot, const juce::SortedSet ¬es, bool notes_changed) override { 82 | 83 | DBGLOG("Linear GETNEXTNOTE called slot = ", int(slot), " changed = ", notes_changed) 84 | 85 | auto note_count = notes.size(); 86 | 87 | if (note_count == 0) { 88 | return -1; 89 | } else if (note_count == 1) { 90 | return notes[0]; 91 | } 92 | 93 | if (notes_changed) { 94 | clock = 0; 95 | } else { 96 | // This is to make up for the fact that if 97 | // the probability function decides to not play 98 | // a note in slot, we won't get called. But we want 99 | // the clock to 'tick' in time with the slot even 100 | // if we don't choose a note. 101 | clock += int(slot) - last_slot; 102 | } 103 | last_slot = int(slot); 104 | 105 | clock = restart ? clock : int(slot); 106 | 107 | DBGLOG(" GNN notes = ", note_count," clock = ", clock, " restart = ", restart); 108 | 109 | 110 | int index = 0; 111 | 112 | if (zigzag) { 113 | // -2 because we don't want to repeat the top and bottom. 114 | auto cycle_length = 2 * note_count - 2; 115 | 116 | index = clock % cycle_length; 117 | DBGLOG(" GNN zigzag cl = ", cycle_length, " index1 = ", index ) 118 | auto correction = ((index - (note_count-1))*2); 119 | auto condition = index >= note_count; 120 | index -= condition * correction; 121 | DBGLOG(" GNN zigzag" , " corr = ", correction, " cond = ", condition, " index = ", index) 122 | 123 | } else { 124 | index = clock % note_count; 125 | } 126 | 127 | if (direction == LinearParameters::Direction::Down) { 128 | index = note_count - 1 - index; 129 | } 130 | 131 | return notes[index]; 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /Source/EditorComponent/HeaderComponent.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #include "HeaderComponent.hpp" 14 | 15 | #include 16 | 17 | const std::string about_text = " " + PLUGIN_NAME_UPPER + " \n" 18 | "Version " + PLUGIN_VERSION + " (" + GIT_HASH + ")\n" 19 | "Copyright (c) 2023 SOlid Fuel\n" 20 | "Licensed under GPL 3 (https://opensource.org/license/gpl-3-0/)\n" 21 | "Source code : https://github.com/mhhollomon/Starp\n" 22 | "\n" 23 | "* This program is free software: you can redistribute it and/or modify it\n" 24 | "* under the terms of the GNU General Public License as published by the \n" 25 | "* Free Software Foundation, either version 3 of the License, or (at your \n" 26 | "* option) any later version. This program is distributed in the hope that it \n" 27 | "* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty\n" 28 | "* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file\n" 29 | "* in the root directory.\n" 30 | "\n" 31 | "\n" 32 | "JUCE Copyright (c) 2022 - Raw Material Software Limited \n" 33 | "Used by permission under GPL 3\n" 34 | "Source Code : https://github.com/juce-framework/JUCE\n" 35 | "\n" 36 | "TinySHA1 Copyright (c) 2012-22 SAURAV MOHAPATRA mohaps@gmail.com\n" 37 | " Permission to use, copy, modify, and distribute this software for any purpose\n" 38 | " with or without fee is hereby granted,\n" 39 | " provided that the above copyright notice and this permission notice appear in all copies." 40 | ; 41 | 42 | HeaderComponent::HeaderComponent() { 43 | nameLabel_.setText (JucePlugin_Name, juce::dontSendNotification); 44 | nameLabel_.setFont(juce::Font(32.0f, juce::Font::bold)); 45 | nameLabel_.setJustificationType(juce::Justification::centred); 46 | addAndMakeVisible (nameLabel_); 47 | 48 | menuButton_.setButtonText("menu"); 49 | menuButton_.changeWidthToFitText(); 50 | menuButton_.setTriggeredOnMouseDown(true); 51 | menuButton_.onClick = [this]() { showMenu_(); }; 52 | 53 | addAndMakeVisible(menuButton_); 54 | 55 | } 56 | 57 | void HeaderComponent::paint (juce::Graphics& g) { 58 | g.fillAll (juce::Colour(0xff060660)); 59 | 60 | } 61 | 62 | //============================================================================== 63 | void HeaderComponent::showMenu_() { 64 | menuButton_.setEnabled(false); 65 | 66 | juce::PopupMenu menu; 67 | 68 | menu.addItem(1, "About"); 69 | 70 | menu.showMenuAsync({}, [this](int r) { processMenu_(r); }); 71 | 72 | } 73 | 74 | //============================================================================== 75 | void HeaderComponent::showAboutBox_() { 76 | auto options = juce::DialogWindow::LaunchOptions(); 77 | 78 | auto* te = new juce::TextEditor(); 79 | 80 | te->setMultiLine(true); 81 | te->setText(about_text, false); 82 | te->setReadOnly(true); 83 | 84 | te->setSize(600, 400); 85 | 86 | options.dialogTitle = "ABOUT " + PLUGIN_NAME_UPPER; 87 | options.content = juce::OptionalScopedPointer(te, true); 88 | 89 | options.launchAsync(); 90 | 91 | } 92 | 93 | //============================================================================== 94 | void HeaderComponent::processMenu_(int results) { 95 | switch (results) { 96 | case 1 : 97 | showAboutBox_(); 98 | break; 99 | default : 100 | break; 101 | } 102 | menuButton_.setEnabled(true); 103 | } 104 | 105 | constexpr int MARGIN = 10; 106 | //============================================================================== 107 | void HeaderComponent::resized() { 108 | 109 | //const auto component_width = getWidth(); 110 | const auto component_height = getHeight(); 111 | 112 | const auto menu_height = component_height / 2.0f; 113 | menuButton_.changeWidthToFitText(int(menu_height)); 114 | menuButton_.setTopLeftPosition({MARGIN, int(menu_height / 2.0f)}); 115 | 116 | nameLabel_.setSize(200, component_height-MARGIN); 117 | nameLabel_.setCentreRelative(0.5f, 0.5f); 118 | } -------------------------------------------------------------------------------- /Source/ProcessorParameters.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #include "ProcessorParameters.hpp" 14 | #include "ParamData.hpp" 15 | 16 | const juce::String DEFAULT_NOTE_SPEED = "1/8"; 17 | constexpr float DEFAULT_BAR_SPEED = 8; 18 | constexpr float DEFAULT_MS_SPEED = 250; 19 | 20 | 21 | //============================================================================ 22 | 23 | const juce::String ProcessorParameters::SPEED_TYPE_ID = "speed_type"; 24 | const juce::String ProcessorParameters::SPEED_NOTE_ID = "speed"; 25 | const juce::String ProcessorParameters::SPEED_BAR_ID = "speed_bar"; 26 | const juce::String ProcessorParameters::SPEED_MSEC_ID = "speed_msec"; 27 | 28 | 29 | 30 | ProcessorParameters::ProcessorParameters(juce::AudioProcessor& processor) { 31 | juce::AudioProcessorValueTreeState::ParameterLayout layout; 32 | 33 | random_parameters.pick_new_key(); 34 | 35 | juce::StringArray speed_choices; 36 | 37 | speed_choices.ensureStorageAllocated(speed_parameter_values.size()); 38 | for (auto const &sv : speed_parameter_values) { 39 | speed_choices.add(sv.name); 40 | } 41 | 42 | int default_speed = speed_choices.indexOf(DEFAULT_NOTE_SPEED); 43 | 44 | auto reverse_bar_range = juce::NormalisableRange(1.0f, 32.0f, 45 | [] (auto rangeStart, auto rangeEnd, auto normalised) 46 | { return juce::jmap (normalised, rangeEnd, rangeStart); }, 47 | [] (auto rangeStart, auto rangeEnd, auto value) 48 | { return juce::jmap (value, rangeEnd, rangeStart, 0.0f, 1.0f); }, 49 | [] (auto, auto, auto value) 50 | { return std::round(value); }); 51 | 52 | auto bar_attributes = juce::AudioParameterFloatAttributes() 53 | .withStringFromValueFunction([](auto value, auto) { return juce::String(value, 0); }); 54 | 55 | // Hosted Parameters 56 | speed = new juce::AudioParameterChoice({SPEED_NOTE_ID, 1}, "Speed in Notes", speed_choices, default_speed); 57 | layout.add(std::unique_ptr(speed)); 58 | 59 | auto gate_norm_range = juce::NormalisableRange{10.0, 200.0, 0.1f}; 60 | gate = new juce::AudioParameterFloat({ "gate", 2 }, "Gate %", gate_norm_range, 100.0); 61 | layout.add(std::unique_ptr(gate)); 62 | 63 | auto gate_range_range = juce::NormalisableRange{0.0, 100.0, 1.0}; 64 | gate_range = new juce::AudioParameterFloat({ "gate_range", 3 }, "Gate Range", gate_range_range, 0.0); 65 | layout.add(std::unique_ptr(gate_range)); 66 | 67 | probability = new juce::AudioParameterInt({"probability", 4}, "Probability", 0, 100, 100); 68 | layout.add(std::unique_ptr(probability)); 69 | 70 | velocity = new juce::AudioParameterInt({"velocity", 5}, "Velocity", 1, 127, 100); 71 | layout.add(std::unique_ptr(velocity)); 72 | 73 | velo_range = new juce::AudioParameterInt({"velocity_range", 6}, "Vel. Range", 0, 64, 0); 74 | layout.add(std::unique_ptr(velo_range)); 75 | 76 | timing_delay = new juce::AudioParameterFloat({ "timing_delay", 7 }, "Delay", 0.0, 30.0, 0.0); 77 | layout.add(std::unique_ptr(timing_delay)); 78 | 79 | timing_advance = new juce::AudioParameterFloat({ "timing_advance", 8 }, "Advance", -30.0, 0.0, 0.0); 80 | layout.add(std::unique_ptr(timing_advance)); 81 | 82 | speed_type = new juce::AudioParameterChoice({SPEED_TYPE_ID, 9}, "Speed Type", SpeedTypes, SpeedType::Note); 83 | layout.add(std::unique_ptr(speed_type)); 84 | 85 | speed_bar = new juce::AudioParameterFloat({SPEED_BAR_ID, 10}, "Speed per Bars", 86 | reverse_bar_range, DEFAULT_BAR_SPEED, bar_attributes); 87 | layout.add(std::unique_ptr(speed_bar)); 88 | 89 | speed_ms = new juce::AudioParameterFloat({ SPEED_MSEC_ID, 11 }, "Speed in msec", 10, 1000, DEFAULT_MS_SPEED); 90 | layout.add(std::unique_ptr(speed_ms)); 91 | 92 | 93 | 94 | 95 | 96 | 97 | apvts = std::unique_ptr( 98 | new juce::AudioProcessorValueTreeState( 99 | processor, nullptr, "STARP-PARAMETERS", std::move(layout))); 100 | } -------------------------------------------------------------------------------- /Source/EditorComponent/RandomAlgoOptionsComponent.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #include "RandomAlgoOptionsComponent.hpp" 14 | 15 | #include "../Starp.hpp" 16 | 17 | //========================================================== 18 | RandomAlgoOptionsComponent::RandomAlgoOptionsComponent(RandomParameters * parms) : 19 | params_{parms} 20 | { 21 | value_.referTo(params_->seed_value); 22 | 23 | seed_listener_.onChange = [this](juce::Value &) { 24 | update_seed_display(); 25 | }; 26 | 27 | value_.addListener(&seed_listener_); 28 | 29 | addAndMakeVisible(keyValueLabel_); 30 | keyValueLabel_.getTextValue().referTo(seed_text_); 31 | update_seed_display(); 32 | keyLabel_.setText ("Seed", juce::dontSendNotification); 33 | keyLabel_.setTooltip("The number used to initialize the random number generator that chooses the notes to play."); 34 | addAndMakeVisible (keyLabel_); 35 | 36 | changeKeyButton_.setButtonText("New Seed"); 37 | changeKeyButton_.onClick = [this]() { changeKey(); }; 38 | addAndMakeVisible(changeKeyButton_); 39 | 40 | replaceButton_.setButtonText("Replace"); 41 | replaceButton_.setTooltip("Normally, the random algorithm does not repeat a note in a set of notes until" 42 | " all notes have been played. When this is on, it will possibly repeat notes sooner (but not back-to-back)."); 43 | replaceButton_.onClick = [this]() { update_replace(); }; 44 | replace_listener_.onChange = [this](juce::Value &v) { 45 | bool rep = bool(v.getValue()); 46 | DBGLOG("RandomAlgoOptionsComponent replace_listener_ = ", rep) 47 | replaceButton_.setToggleState(rep, juce::sendNotification); 48 | }; 49 | replace_listener_.onChange(params_->replace); 50 | params_->replace.addListener(&replace_listener_); 51 | 52 | addAndMakeVisible(replaceButton_); 53 | 54 | } 55 | 56 | //========================================================== 57 | RandomAlgoOptionsComponent::~RandomAlgoOptionsComponent() { 58 | params_->replace.removeListener(&replace_listener_); 59 | // don't need to remove the listener from value_ since 60 | // we own it. Its won't outlive this object. 61 | } 62 | 63 | //========================================================== 64 | void RandomAlgoOptionsComponent::update_seed_display() { 65 | DBGLOG("RandomAlgoOptionsComponent::update_seed_display called") 66 | seed_text_ = juce::String::toHexString(juce::int64(value_.getValue())); 67 | } 68 | 69 | //============================================================================== 70 | void RandomAlgoOptionsComponent::changeKey() { 71 | DBGLOG("RandomAlgoOptionsComponent::changeKey called") 72 | params_->pick_new_key(); 73 | } 74 | 75 | void RandomAlgoOptionsComponent::update_replace() { 76 | 77 | auto r = replaceButton_.getToggleState(); 78 | DBGLOG("RandomAlgoOptionsComponent::update_replace = ", r) 79 | params_->replace = r; 80 | } 81 | 82 | 83 | //========================================================== 84 | void RandomAlgoOptionsComponent::paint(juce::Graphics& g) { 85 | DBGLOG("RandomAlgoOptionsComponent::paint called"); 86 | g.fillAll (getLookAndFeel().findColour (juce::PropertyComponent::backgroundColourId)); 87 | 88 | } 89 | 90 | //========================================================== 91 | void RandomAlgoOptionsComponent::resized() { 92 | 93 | DBGLOG("RandomAlgoOptionsComponent::resized called"); 94 | 95 | juce::Grid grid; 96 | 97 | using Track = juce::Grid::TrackInfo; 98 | using Fr = juce::Grid::Fr; 99 | using GridItem = juce::GridItem; 100 | 101 | grid.alignItems = juce::Grid::AlignItems::center; 102 | grid.justifyContent = juce::Grid::JustifyContent::start; 103 | grid.justifyItems = juce::Grid::JustifyItems::start; 104 | grid.templateColumns = { Track (Fr (1)), Track(Fr(2)), Track (Fr (2)), Track (Fr (1))}; 105 | 106 | grid.templateRows.add(Track (Fr (1))); 107 | grid.items.add(GridItem(keyLabel_)); 108 | grid.items.add(GridItem(keyValueLabel_)); 109 | 110 | auto h = int(getHeight() * 0.75); 111 | auto w = changeKeyButton_.getBestWidthForHeight(h); 112 | grid.items.add(GridItem(changeKeyButton_).withHeight(float(h)).withWidth(float(w))); 113 | 114 | grid.items.add(GridItem(replaceButton_)); 115 | 116 | grid.performLayout (getLocalBounds()); 117 | 118 | } 119 | -------------------------------------------------------------------------------- /Source/EditorComponent/PropertyComponent.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include "OverlayComponent.hpp" 16 | #include "AlgoChoiceComponent.hpp" 17 | #include "RandomAlgoOptionsComponent.hpp" 18 | #include "LinearAlgoOptionsComponent.hpp" 19 | #include "../ProcessorParameters.hpp" 20 | 21 | #include 22 | #include 23 | #include 24 | 25 | using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment; 26 | using namespace solidfuel; 27 | 28 | class SpeedTypeChoiceComponent : public juce::ChoicePropertyComponent { 29 | private : 30 | juce::Value value_ptr_; 31 | ValueListener listener_; 32 | 33 | public : 34 | SpeedTypeChoiceComponent() : 35 | juce::ChoicePropertyComponent("") 36 | { 37 | 38 | choices = SpeedTypes; 39 | listener_.onChange = [this](juce::Value &){ refresh(); }; 40 | refresh(); 41 | } 42 | 43 | void setValue(juce::Value &ptr) { 44 | value_ptr_.referTo(ptr); 45 | value_ptr_.addListener(&listener_); 46 | } 47 | 48 | virtual void setIndex(int newIndex) override { 49 | value_ptr_.setValue(newIndex); 50 | } 51 | virtual int getIndex() const override { 52 | return int(value_ptr_.getValue()); 53 | } 54 | 55 | 56 | /* 57 | * Don't paint the label 58 | */ 59 | 60 | void paint (juce::Graphics& g) override { 61 | auto& lf = getLookAndFeel(); 62 | 63 | lf.drawPropertyComponentBackground (g, getWidth(), getHeight(), *this); 64 | } 65 | 66 | void resized() override { 67 | if (auto c = getChildComponent (0)) { 68 | 69 | auto bounds = getLocalBounds(); 70 | bounds.reduce(8, 0); 71 | c->setBounds(bounds); 72 | } 73 | } 74 | 75 | 76 | }; 77 | 78 | 79 | //============================================================== 80 | class PropertyComponent : public juce::Component { 81 | 82 | 83 | public: 84 | 85 | PropertyComponent(ProcessorParameters *params); 86 | 87 | void paint(juce::Graphics&) override; 88 | void resized() override; 89 | 90 | private: 91 | 92 | using BCO = BoxComponent::Orientation; 93 | 94 | ProcessorParameters *params_ = nullptr; 95 | 96 | juce::Value speed_type_value_{SpeedType::Note}; 97 | 98 | 99 | BoxComponent speedBox_{BCO::Horizontal, true}; 100 | 101 | SpeedTypeChoiceComponent speedType_; 102 | 103 | OverlayComponent speedComponent_; 104 | 105 | juce::Slider speedNoteSlider_; 106 | juce::Slider speedBarSlider_; 107 | juce::Slider speedMSecSlider_; 108 | std::unique_ptr speedNoteAttachment_; 109 | std::unique_ptr speedBarAttachment_; 110 | std::unique_ptr speedMSecAttachment_; 111 | 112 | ValueListener speed_type_listener_; 113 | 114 | juce::Label probabilityLabel_; 115 | juce::Slider probabilitySlider_; 116 | std::unique_ptr probabilityAttachment_; 117 | 118 | BoxComponent gateBox_{BCO::Horizontal}; 119 | juce::Label gateLabel_; 120 | juce::Slider gateSlider_; 121 | std::unique_ptr gateAttachment_; 122 | 123 | BoxComponent gateRangeBox_{BCO::Horizontal}; 124 | juce::Label gateRangeLabel_; 125 | juce::Slider gateRangeSlider_; 126 | std::unique_ptr gateRangeAttachment_; 127 | 128 | BoxComponent veloBox_{BCO::Horizontal}; 129 | juce::Label veloLabel_; 130 | juce::Slider veloSlider_; 131 | std::unique_ptr veloAttachment_; 132 | 133 | BoxComponent veloRangeBox_{BCO::Horizontal}; 134 | juce::Label veloRangeLabel_; 135 | juce::Slider veloRangeSlider_; 136 | std::unique_ptr veloRangeAttachment_; 137 | 138 | BoxComponent advanceBox_{BCO::Horizontal}; 139 | juce::Label advanceLabel_; 140 | juce::Slider advanceSlider_; 141 | std::unique_ptr advanceAttachment_; 142 | 143 | BoxComponent delayBox_{BCO::Horizontal}; 144 | juce::Label delayLabel_; 145 | juce::Slider delaySlider_; 146 | std::unique_ptr delayAttachment_; 147 | 148 | BoxComponent gateGroup_{BCO::Vertical, true}; 149 | BoxComponent veloGroup_{BCO::Vertical, true}; 150 | BoxComponent timingGroup_{BCO::Vertical, true}; 151 | 152 | void update_speed_type(SpeedType st); 153 | 154 | //========================================================== 155 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PropertyComponent) 156 | }; -------------------------------------------------------------------------------- /Source/EditorComponent/LinearAlgoOptionsComponent.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | 14 | #include "LinearAlgoOptionsComponent.hpp" 15 | 16 | constexpr int POSITION_GID = 1001; 17 | 18 | LinearAlgoOptionsComponent::LinearAlgoOptionsComponent(LinearParameters * parms) : params_(parms) { 19 | 20 | up_button_.setButtonText("Up"); 21 | up_button_.setClickingTogglesState(true); 22 | up_button_.setRadioGroupId(POSITION_GID); 23 | up_button_.setConnectedEdges(juce::Button::ConnectedEdgeFlags::ConnectedOnRight); 24 | addAndMakeVisible(up_button_); 25 | up_button_.onClick = [this]() { update_direction(); }; 26 | 27 | 28 | down_button_.setButtonText("Down"); 29 | down_button_.setClickingTogglesState(true); 30 | down_button_.setRadioGroupId(POSITION_GID); 31 | down_button_.setConnectedEdges(juce::Button::ConnectedEdgeFlags::ConnectedOnLeft); 32 | addAndMakeVisible(down_button_); 33 | 34 | zigzag_button_.setButtonText("Zigzag"); 35 | addAndMakeVisible(zigzag_button_); 36 | zigzag_button_.onClick = [this]() { update_zigzag(); }; 37 | 38 | restart_button_.setButtonText("Restart"); 39 | restart_button_.setTooltip("Restart the sequence whenever the midi notes change"); 40 | addAndMakeVisible(restart_button_); 41 | restart_button_.onClick = [this]() { update_restart(); }; 42 | 43 | direction_listener_.onChange = [this](juce::Value &v) { 44 | if (int(v.getValue()) == LinearParameters::Direction::Up) { 45 | up_button_.setToggleState(true, juce::sendNotification); 46 | } else { 47 | down_button_.setToggleState(true, juce::sendNotification); 48 | } 49 | }; 50 | // force the GUI to match the model 51 | direction_listener_.onChange(params_->direction); 52 | 53 | params_->direction.addListener(&direction_listener_); 54 | 55 | zigzag_listener_.onChange = [this](juce::Value &) { 56 | zigzag_button_.setToggleState(params_->get_zigzag(), juce::sendNotification); 57 | }; 58 | 59 | // force the GUI to match the model 60 | zigzag_listener_.onChange(params_->zigzag); 61 | params_->zigzag.addListener(&zigzag_listener_); 62 | 63 | restart_listener_.onChange = [this](juce::Value &) { 64 | restart_button_.setToggleState(params_->get_restart(), juce::sendNotification); 65 | }; 66 | 67 | // force the GUI to match the model 68 | restart_listener_.onChange(params_->restart); 69 | params_->restart.addListener(&restart_listener_); 70 | } 71 | 72 | LinearAlgoOptionsComponent::~LinearAlgoOptionsComponent() { 73 | params_->direction.removeListener(&direction_listener_); 74 | params_->zigzag.removeListener(&zigzag_listener_); 75 | params_->restart.removeListener(&restart_listener_); 76 | } 77 | 78 | void LinearAlgoOptionsComponent::update_direction() { 79 | auto state = up_button_.getToggleState() ? 80 | LinearParameters::Direction::Up : 81 | LinearParameters::Direction::Down ; 82 | 83 | 84 | params_->direction = state; 85 | } 86 | 87 | void LinearAlgoOptionsComponent::update_zigzag() { 88 | params_->zigzag = zigzag_button_.getToggleState(); 89 | } 90 | 91 | void LinearAlgoOptionsComponent::update_restart() { 92 | params_->restart = restart_button_.getToggleState(); 93 | } 94 | 95 | //============================================================== 96 | void LinearAlgoOptionsComponent::paint(juce::Graphics&g) { 97 | DBGLOG("LinearAlgoOptionsComponent::paint called"); 98 | g.fillAll (getLookAndFeel().findColour (juce::PropertyComponent::backgroundColourId)); 99 | 100 | } 101 | 102 | //========================================================== 103 | void LinearAlgoOptionsComponent::resized() { 104 | 105 | DBGLOG("LinearAlgoOptionsComponent::resized called") 106 | 107 | using Grid = juce::Grid; 108 | 109 | Grid grid; 110 | 111 | using Track = Grid::TrackInfo; 112 | using Fr = Grid::Fr; 113 | using GridItem = juce::GridItem; 114 | 115 | grid.alignItems = juce::Grid::AlignItems::center; 116 | grid.justifyContent = juce::Grid::JustifyContent::start; 117 | grid.justifyItems = Grid::JustifyItems::start; 118 | grid.templateColumns = { Track (Fr (1)), Track(Fr(1)), Track (Fr (2)), Track (Fr (2)) }; 119 | 120 | grid.templateRows.add(Track (Fr (1))); 121 | auto dir_button_height = float(getHeight() * 0.75f); 122 | 123 | // use the prefered width of the "Down" button since it should be larger. 124 | float dir_button_width = float(down_button_.getBestWidthForHeight(int(dir_button_height))); 125 | 126 | grid.items.add(GridItem(up_button_).withHeight(dir_button_height) 127 | .withWidth(dir_button_width) 128 | .withJustifySelf(GridItem::JustifySelf::end) ); 129 | 130 | grid.items.add(GridItem(down_button_).withHeight(dir_button_height).withWidth(dir_button_width)); 131 | 132 | grid.items.add(GridItem(zigzag_button_)); 133 | grid.items.add(GridItem(restart_button_)); 134 | 135 | grid.performLayout (getLocalBounds()); 136 | 137 | } 138 | 139 | 140 | -------------------------------------------------------------------------------- /Source/EditorComponent/SpiralAlgoOptionsComponent.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | 14 | #include "SpiralAlgoOptionsComponent.hpp" 15 | 16 | constexpr int POSITION_GID = 1002; 17 | constexpr int DIRECTION_GID = 1003; 18 | 19 | using SD = SpiralParameters::Direction; 20 | 21 | SpiralAlgoOptionsComponent::SpiralAlgoOptionsComponent(SpiralParameters * parms) : params_(parms) { 22 | 23 | top_button_.setButtonText("Top"); 24 | top_button_.setClickingTogglesState(true); 25 | top_button_.setRadioGroupId(POSITION_GID); 26 | top_button_.setConnectedEdges(juce::Button::ConnectedEdgeFlags::ConnectedOnRight); 27 | addAndMakeVisible(top_button_); 28 | top_button_.onClick = [this]() { update_position(); }; 29 | 30 | 31 | bottom_button_.setButtonText("Bottom"); 32 | bottom_button_.setClickingTogglesState(true); 33 | bottom_button_.setRadioGroupId(POSITION_GID); 34 | bottom_button_.setConnectedEdges(juce::Button::ConnectedEdgeFlags::ConnectedOnLeft); 35 | addAndMakeVisible(bottom_button_); 36 | 37 | direction_group_.setGroupId(DIRECTION_GID); 38 | 39 | in_button_.setButtonText("In"); 40 | in_button_.onClick = [this]() { update_direction(SD::In); }; 41 | direction_group_.add(in_button_); 42 | 43 | out_button_.setButtonText("Out"); 44 | out_button_.onClick = [this]() { update_direction(SD::Out); }; 45 | direction_group_.add(out_button_); 46 | 47 | inout_button_.setButtonText("InOut"); 48 | inout_button_.onClick = [this]() { update_direction(SD::InOut); }; 49 | direction_group_.add(inout_button_); 50 | 51 | outin_button_.setButtonText("OutIn"); 52 | outin_button_.onClick = [this]() { update_direction(SD::OutIn); }; 53 | direction_group_.add(outin_button_); 54 | 55 | addAndMakeVisible(direction_group_); 56 | 57 | 58 | position_listener_.onChange = [this](juce::Value &v) { 59 | if (int(v.getValue()) == SpiralParameters::StartPosition::Top) { 60 | top_button_.setToggleState(true, juce::sendNotification); 61 | } else { 62 | bottom_button_.setToggleState(true, juce::sendNotification); 63 | } 64 | }; 65 | // force the GUI to match the model 66 | position_listener_.onChange(params_->start_position); 67 | 68 | params_->start_position.addListener(&position_listener_); 69 | 70 | direction_listener_.onChange = [this](juce::Value &v) { 71 | int new_dir = int(v.getValue()); 72 | switch (new_dir) { 73 | case SD::In : 74 | in_button_.setToggleState(true, juce::sendNotification); 75 | break; 76 | case SD::Out : 77 | out_button_.setToggleState(true, juce::sendNotification); 78 | break; 79 | case SD::InOut : 80 | inout_button_.setToggleState(true, juce::sendNotification); 81 | break; 82 | case SD::OutIn : 83 | outin_button_.setToggleState(true, juce::sendNotification); 84 | break; 85 | default : 86 | jassertfalse; 87 | } 88 | }; 89 | 90 | // force the GUI to match the model 91 | direction_listener_.onChange(params_->direction); 92 | 93 | params_->direction.addListener(&direction_listener_); 94 | 95 | 96 | 97 | } 98 | 99 | SpiralAlgoOptionsComponent::~SpiralAlgoOptionsComponent() { 100 | params_->start_position.removeListener(&position_listener_); 101 | params_->direction.removeListener(&direction_listener_); 102 | } 103 | 104 | void SpiralAlgoOptionsComponent::update_position() { 105 | auto state = top_button_.getToggleState() ? 106 | SpiralParameters::StartPosition::Top : 107 | SpiralParameters::StartPosition::Bottom ; 108 | 109 | 110 | params_->start_position = state; 111 | } 112 | 113 | void SpiralAlgoOptionsComponent::update_direction(SpiralParameters::Direction direction) { 114 | params_->direction = direction; 115 | } 116 | 117 | 118 | 119 | //============================================================== 120 | void SpiralAlgoOptionsComponent::paint(juce::Graphics&g) { 121 | g.fillAll (getLookAndFeel().findColour (juce::PropertyComponent::backgroundColourId)); 122 | 123 | } 124 | 125 | //========================================================== 126 | void SpiralAlgoOptionsComponent::resized() { 127 | 128 | DBGLOG("SpiralAlgoOptionsComponent::resized called") 129 | 130 | using Grid = juce::Grid; 131 | 132 | Grid grid; 133 | 134 | using Track = Grid::TrackInfo; 135 | using Fr = Grid::Fr; 136 | using GridItem = juce::GridItem; 137 | 138 | grid.alignItems = juce::Grid::AlignItems::center; 139 | grid.justifyContent = juce::Grid::JustifyContent::start; 140 | grid.justifyItems = Grid::JustifyItems::start; 141 | grid.templateColumns = { Track (Fr (1)), Track(Fr(1)), Track (Fr (3)), Track (Fr (1)) }; 142 | 143 | grid.templateRows.add(Track (Fr (1))); 144 | auto dir_button_height = float(getHeight() * 0.75f); 145 | 146 | // use the prefered width of the "Bottom" button since it should be larger. 147 | float dir_button_width = float(bottom_button_.getBestWidthForHeight(int(dir_button_height))); 148 | 149 | grid.items.add(GridItem(top_button_).withHeight(dir_button_height) 150 | .withWidth(dir_button_width) 151 | .withJustifySelf(GridItem::JustifySelf::end) ); 152 | 153 | grid.items.add(GridItem(bottom_button_).withHeight(dir_button_height).withWidth(dir_button_width)); 154 | 155 | grid.items.add(GridItem(direction_group_).withHeight(dir_button_height)); 156 | 157 | grid.performLayout (getLocalBounds()); 158 | 159 | } 160 | -------------------------------------------------------------------------------- /Source/PluginProcessor.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include "Algorithm.hpp" 16 | #include "ParamData.hpp" 17 | #include "ProcessorParameters.hpp" 18 | 19 | #include "position_data.hpp" 20 | #include 21 | 22 | #include 23 | 24 | #include 25 | 26 | using namespace solidfuel; 27 | 28 | using int64 = juce::int64; 29 | 30 | struct played_note { 31 | // midi note value 32 | int note_value; 33 | 34 | // ending time in hires timer ticks 35 | int64_t end_tick; 36 | 37 | }; 38 | 39 | bool operator==(const played_note& lhs, const played_note& rhs); 40 | bool operator<(const played_note& lhs, const played_note& rhs); 41 | 42 | 43 | //============================================================================ 44 | struct schedule { 45 | double slot_number; 46 | double start; 47 | }; 48 | 49 | bool operator==(const schedule& lhs, const schedule& rhs); 50 | bool operator<(const schedule& lhs, const schedule& rhs); 51 | 52 | //============================================================================== 53 | class PluginProcessor : public juce::AudioProcessor { 54 | 55 | public: 56 | //========================================================================== 57 | // These are in setup.cpp 58 | PluginProcessor(); 59 | ~PluginProcessor() override; 60 | 61 | juce::AudioProcessor::BusesProperties getDefaultProperties(); 62 | 63 | juce::AudioProcessorEditor* createEditor() override; 64 | bool hasEditor() const override { return true; } 65 | 66 | //========================================================================== 67 | void prepareToPlay (double sampleRate, int samplesPerBlock) override; 68 | void releaseResources() override; 69 | 70 | 71 | void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; 72 | void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; 73 | 74 | void processMidi(int sample_count, int64 tick_count, juce::MidiBuffer&); 75 | 76 | //========================================================================== 77 | 78 | //========================================================================== 79 | // These are in setup.cpp 80 | 81 | bool isBusesLayoutSupported (const BusesLayout&) const override { return true; } 82 | 83 | const juce::String getName() const override {return JucePlugin_Name;} 84 | 85 | bool acceptsMidi() const override { return true; } 86 | bool producesMidi() const override { return true; } 87 | // depends on the host we are in 88 | bool isMidiEffect() const override; 89 | double getTailLengthSeconds() const override { return 0.0; } 90 | 91 | int getNumPrograms() override { return 1; } 92 | int getCurrentProgram() override { return 0; } 93 | void setCurrentProgram(int) override {} 94 | const juce::String getProgramName(int) override { return {}; } 95 | void changeProgramName(int, const juce::String&) override {} 96 | 97 | 98 | //========================================================================== 99 | void getStateInformation (juce::MemoryBlock& destData) override; 100 | void setStateInformation (const void* data, int sizeInBytes) override; 101 | 102 | private: 103 | 104 | 105 | ProcessorParameters parameters_; 106 | 107 | //========================================================================== 108 | 109 | 110 | // Used to set other things based on the Host 111 | juce::PluginHostType host_type; 112 | 113 | // Sample rate 114 | double sample_rate_; 115 | 116 | position_data pd; 117 | 118 | // Set of notes we can choose from if we need 119 | // to schedule something. 120 | juce::SortedSet incoming_notes_; 121 | 122 | // Has incoming_notes_ changed since the last 123 | // time we called the note picking algoirthm ? 124 | bool notes_changed_ = false; 125 | 126 | // Notes we have sent the note-on for but need to wait 127 | // to send the note-off 128 | juce::Array active_notes_; 129 | 130 | // Notes we are waiting to send the note-on for. 131 | juce::Array scheduled_notes_; 132 | 133 | 134 | bool algo_changed_ = false; 135 | void update_algorithm(int new_algo); 136 | std::unique_ptr algo_obj_; 137 | 138 | double last_scheduled_slot_number_ = -1.0; 139 | 140 | bool last_play_state_ = false; 141 | 142 | double getSpeedFactor(double bpm, 143 | const juce::AudioPlayHead::TimeSignature &time_sig); 144 | double getGate(double slot); 145 | 146 | // Last time in millisecs that processBlock was called. 147 | // Used to detect bypass. If we haven't been called in 148 | // 100 ms then assume we've been bypassed. 149 | long long last_block_call_ = -1; 150 | 151 | // position when processBlock was last called. 152 | // Used to detect looping. 153 | double last_position_ = -1; 154 | 155 | double fake_clock_sample_count_ = 0; 156 | 157 | void update_position_data(int64 tick_count); 158 | std::optionalmaybe_play_note(double for_slot, 159 | double start_pos); 160 | 161 | void schedule_note(double current_pos, double slot_number, 162 | bool can_advance); 163 | 164 | void reset_data(bool clear_incoming = true); 165 | 166 | void parseCurrentXml(const juce::XmlElement * elem); 167 | void parseOriginalXml(const juce::XmlElement * elem); 168 | 169 | juce::SharedResourcePointer tooltipWindow; 170 | 171 | ValueListener algo_listener_; 172 | 173 | public: 174 | ProcessorParameters* getParameters() { return ¶meters_; } 175 | 176 | //========================================================================== 177 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginProcessor) 178 | }; 179 | -------------------------------------------------------------------------------- /external/TinySHA1/TinySHA1.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * TinySHA1 - a header only implementation of the SHA1 algorithm in C++. Based 4 | * on the implementation in boost::uuid::details. 5 | * 6 | * SHA1 Wikipedia Page: http://en.wikipedia.org/wiki/SHA-1 7 | * 8 | * Copyright (c) 2012-22 SAURAV MOHAPATRA 9 | * 10 | * Permission to use, copy, modify, and distribute this software for any 11 | * purpose with or without fee is hereby granted, provided that the above 12 | * copyright notice and this permission notice appear in all copies. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 15 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 16 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 17 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 18 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 19 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 20 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 21 | */ 22 | #ifndef _TINY_SHA1_HPP_ 23 | #define _TINY_SHA1_HPP_ 24 | #include 25 | #include 26 | #include 27 | #include 28 | namespace sha1 29 | { 30 | class SHA1 31 | { 32 | public: 33 | typedef uint32_t digest32_t[5]; 34 | typedef uint8_t digest8_t[20]; 35 | inline static uint32_t LeftRotate(uint32_t value, size_t count) { 36 | return (value << count) ^ (value >> (32-count)); 37 | } 38 | SHA1(){ reset(); } 39 | virtual ~SHA1() {} 40 | SHA1(const SHA1& s) { *this = s; } 41 | const SHA1& operator = (const SHA1& s) { 42 | memcpy(m_digest, s.m_digest, 5 * sizeof(uint32_t)); 43 | memcpy(m_block, s.m_block, 64); 44 | m_blockByteIndex = s.m_blockByteIndex; 45 | m_byteCount = s.m_byteCount; 46 | return *this; 47 | } 48 | SHA1& reset() { 49 | m_digest[0] = 0x67452301; 50 | m_digest[1] = 0xEFCDAB89; 51 | m_digest[2] = 0x98BADCFE; 52 | m_digest[3] = 0x10325476; 53 | m_digest[4] = 0xC3D2E1F0; 54 | m_blockByteIndex = 0; 55 | m_byteCount = 0; 56 | return *this; 57 | } 58 | SHA1& processByte(uint8_t octet) { 59 | this->m_block[this->m_blockByteIndex++] = octet; 60 | ++this->m_byteCount; 61 | if(m_blockByteIndex == 64) { 62 | this->m_blockByteIndex = 0; 63 | processBlock(); 64 | } 65 | return *this; 66 | } 67 | SHA1& processBlock(const void* const start, const void* const end) { 68 | const uint8_t* begin = static_cast(start); 69 | const uint8_t* finish = static_cast(end); 70 | while(begin != finish) { 71 | processByte(*begin); 72 | begin++; 73 | } 74 | return *this; 75 | } 76 | SHA1& processBytes(const void* const data, size_t len) { 77 | const uint8_t* block = static_cast(data); 78 | processBlock(block, block + len); 79 | return *this; 80 | } 81 | const uint32_t* getDigest(digest32_t digest) { 82 | size_t bitCount = this->m_byteCount * 8; 83 | processByte(0x80); 84 | if (this->m_blockByteIndex > 56) { 85 | while (m_blockByteIndex != 0) { 86 | processByte(0); 87 | } 88 | while (m_blockByteIndex < 56) { 89 | processByte(0); 90 | } 91 | } else { 92 | while (m_blockByteIndex < 56) { 93 | processByte(0); 94 | } 95 | } 96 | processByte(0); 97 | processByte(0); 98 | processByte(0); 99 | processByte(0); 100 | processByte( static_cast((bitCount>>24) & 0xFF)); 101 | processByte( static_cast((bitCount>>16) & 0xFF)); 102 | processByte( static_cast((bitCount>>8 ) & 0xFF)); 103 | processByte( static_cast((bitCount) & 0xFF)); 104 | 105 | memcpy(digest, m_digest, 5 * sizeof(uint32_t)); 106 | return digest; 107 | } 108 | const uint8_t* getDigestBytes(digest8_t digest) { 109 | digest32_t d32; 110 | getDigest(d32); 111 | size_t di = 0; 112 | digest[di++] = ((d32[0] >> 24) & 0xFF); 113 | digest[di++] = ((d32[0] >> 16) & 0xFF); 114 | digest[di++] = ((d32[0] >> 8) & 0xFF); 115 | digest[di++] = ((d32[0]) & 0xFF); 116 | 117 | digest[di++] = ((d32[1] >> 24) & 0xFF); 118 | digest[di++] = ((d32[1] >> 16) & 0xFF); 119 | digest[di++] = ((d32[1] >> 8) & 0xFF); 120 | digest[di++] = ((d32[1]) & 0xFF); 121 | 122 | digest[di++] = ((d32[2] >> 24) & 0xFF); 123 | digest[di++] = ((d32[2] >> 16) & 0xFF); 124 | digest[di++] = ((d32[2] >> 8) & 0xFF); 125 | digest[di++] = ((d32[2]) & 0xFF); 126 | 127 | digest[di++] = ((d32[3] >> 24) & 0xFF); 128 | digest[di++] = ((d32[3] >> 16) & 0xFF); 129 | digest[di++] = ((d32[3] >> 8) & 0xFF); 130 | digest[di++] = ((d32[3]) & 0xFF); 131 | 132 | digest[di++] = ((d32[4] >> 24) & 0xFF); 133 | digest[di++] = ((d32[4] >> 16) & 0xFF); 134 | digest[di++] = ((d32[4] >> 8) & 0xFF); 135 | digest[di++] = ((d32[4]) & 0xFF); 136 | return digest; 137 | } 138 | 139 | protected: 140 | void processBlock() { 141 | uint32_t w[80]; 142 | for (size_t i = 0; i < 16; i++) { 143 | w[i] = (uint32_t(m_block[i*4 + 0]) << 24); 144 | w[i] |= (uint32_t(m_block[i*4 + 1]) << 16); 145 | w[i] |= (uint32_t(m_block[i*4 + 2]) << 8); 146 | w[i] |= (m_block[i*4 + 3]); 147 | } 148 | for (size_t i = 16; i < 80; i++) { 149 | w[i] = LeftRotate((w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16]), 1); 150 | } 151 | 152 | uint32_t a = m_digest[0]; 153 | uint32_t b = m_digest[1]; 154 | uint32_t c = m_digest[2]; 155 | uint32_t d = m_digest[3]; 156 | uint32_t e = m_digest[4]; 157 | 158 | for (std::size_t i=0; i<80; ++i) { 159 | uint32_t f = 0; 160 | uint32_t k = 0; 161 | 162 | if (i<20) { 163 | f = (b & c) | (~b & d); 164 | k = 0x5A827999; 165 | } else if (i<40) { 166 | f = b ^ c ^ d; 167 | k = 0x6ED9EBA1; 168 | } else if (i<60) { 169 | f = (b & c) | (b & d) | (c & d); 170 | k = 0x8F1BBCDC; 171 | } else { 172 | f = b ^ c ^ d; 173 | k = 0xCA62C1D6; 174 | } 175 | uint32_t temp = LeftRotate(a, 5) + f + e + k + w[i]; 176 | e = d; 177 | d = c; 178 | c = LeftRotate(b, 30); 179 | b = a; 180 | a = temp; 181 | } 182 | 183 | m_digest[0] += a; 184 | m_digest[1] += b; 185 | m_digest[2] += c; 186 | m_digest[3] += d; 187 | m_digest[4] += e; 188 | } 189 | private: 190 | digest32_t m_digest; 191 | uint8_t m_block[64]; 192 | size_t m_blockByteIndex; 193 | size_t m_byteCount; 194 | }; 195 | } 196 | #endif 197 | -------------------------------------------------------------------------------- /Source/Algorithms/SpiralAlgorithm.hpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #pragma once 14 | 15 | #include "AlgoBase.hpp" 16 | #include "../AlgorithmParameters.hpp" 17 | 18 | #include "../Starp.hpp" 19 | 20 | #include 21 | 22 | using namespace solidfuel; 23 | 24 | class SpiralAlgorithm : public AlgorithmBase { 25 | 26 | private: 27 | SpiralParameters * p_; 28 | 29 | int direction = SpiralParameters::Direction::In; 30 | int start_position = SpiralParameters::StartPosition::Top; 31 | 32 | int clock_ = -1; 33 | 34 | int last_slot = 0; 35 | 36 | juce::Array index_map_; 37 | 38 | 39 | ValueListener direction_listener_; 40 | ValueListener start_position_listener_; 41 | 42 | void update_parameters() { 43 | DBGLOG("SpiralAlgorithm::update_parameters called") 44 | direction = p_->get_direction(); 45 | start_position = p_->get_start_position(); 46 | index_map_.clearQuick(); 47 | DBGLOG(" direction = ", direction, "; start =", start_position) 48 | } 49 | 50 | public : 51 | SpiralAlgorithm(SpiralParameters * p) : p_{p} { 52 | update_parameters(); 53 | 54 | direction_listener_.onChange = [this](juce::Value &) { 55 | update_parameters(); 56 | }; 57 | 58 | p_->direction.addListener(&direction_listener_); 59 | 60 | start_position_listener_.onChange = [this](juce::Value &) { 61 | update_parameters(); 62 | }; 63 | 64 | p_->start_position.addListener(&start_position_listener_); 65 | 66 | } 67 | 68 | ~SpiralAlgorithm() override { 69 | p_->direction.removeListener(&direction_listener_); 70 | p_->start_position.removeListener(&start_position_listener_); 71 | } 72 | 73 | Algorithm get_algo() const override { return Algorithm::Spiral; } 74 | 75 | void reset() override { 76 | index_map_.clearQuick(); 77 | clock_ = 0; 78 | } 79 | 80 | int getNextNote(double slot, const juce::SortedSet ¬es, bool notes_changed) override { 81 | 82 | auto note_count = notes.size(); 83 | 84 | DBGLOG("Spiral GETNEXTNOTE called slot = ", int(slot), "; changed = ", notes_changed, "; count = ", note_count) 85 | 86 | if (note_count == 0) { 87 | return -1; 88 | } else if (note_count == 1) { 89 | return notes[0]; 90 | } 91 | 92 | if (notes_changed) { 93 | clock_ = 0; 94 | } else { 95 | // This is to make up for the fact that if 96 | // the probability function decides to not play 97 | // a note in slot, we won't get called. But we want 98 | // the clock to 'tick' in time with the slot even 99 | // if we don't choose a note. 100 | clock_ += int(slot) - last_slot; 101 | } 102 | last_slot = int(slot); 103 | 104 | DBGLOG(" GNN notes = ", note_count," clock = ", clock_ ); 105 | 106 | if (notes_changed || index_map_.isEmpty()) { 107 | index_map_.clearQuick(); 108 | switch (direction) { 109 | using SD = SpiralParameters::Direction; 110 | case SD::In : 111 | render_in_algorithm(note_count, false); 112 | break; 113 | case SD::Out : 114 | render_out_algorithm(note_count, false); 115 | break; 116 | case SD::InOut : 117 | render_in_algorithm(note_count, false); 118 | render_out_algorithm(note_count, true); 119 | break; 120 | case SD::OutIn : 121 | render_out_algorithm(note_count, false); 122 | render_in_algorithm(note_count, true); 123 | break; 124 | } 125 | } 126 | 127 | 128 | int map_index = clock_ % index_map_.size(); 129 | int index = index_map_[map_index]; 130 | DBGLOG(" GNN : map_index = ", map_index, "; index = ", index) 131 | 132 | return notes[index]; 133 | } 134 | 135 | private : 136 | void render_in_algorithm(int note_count, bool second) { 137 | DBGLOG("SpiralAlogrithm::render_in_algorithm called.") 138 | DBGLOG(" ria: position = ", start_position, "; note_count = ", note_count, "; second = ", second) 139 | 140 | int pointers[2] = { 141 | 0, note_count-1 142 | }; 143 | 144 | int added = 0; 145 | int position = start_position; 146 | 147 | DBGLOG(" ria: p0 = ", pointers[0], "; p1 = ", pointers[1], "; position = ", position) 148 | 149 | while (added < note_count ) { 150 | 151 | // if we are the second half, skip our first note 152 | if (!second || (added > 0)) 153 | index_map_.add(pointers[position]); 154 | added += 1; 155 | pointers[position] += (1-2*position); 156 | position = 1-position; 157 | DBGLOG(" ria: p0 = ", pointers[0], "; p1 = ", pointers[1], "; position = ", position) 158 | } 159 | } 160 | 161 | void render_out_algorithm(int note_count, bool second) { 162 | DBGLOG("SpiralAlogrithm::render_out_algorithm called.") 163 | DBGLOG(" roa: start position = ", start_position, "; note_count = ", note_count, "; second = ", second) 164 | 165 | using SP = SpiralParameters::StartPosition; 166 | 167 | int mid = (note_count -1)/ 2; 168 | bool odd = (note_count %2 == 1); 169 | 170 | int pointers[2]; 171 | 172 | if (start_position == SP::Bottom) { 173 | pointers[SP::Bottom] = mid; 174 | pointers[SP::Top] = mid + 1; 175 | } else { 176 | pointers[SP::Bottom] = mid - odd; 177 | pointers[SP::Top] = mid + (!odd); 178 | } 179 | 180 | 181 | int added = 0; 182 | int position; 183 | if (odd) { 184 | position = start_position; 185 | } else { 186 | position = 1 - start_position; 187 | } 188 | 189 | DBGLOG(" roa: mid = ", mid, "; odd = ", odd, "; p0 = ", pointers[0], "; p1 = ", pointers[1], "; position = ", position) 190 | 191 | while (added < note_count ) { 192 | 193 | // if we are the second half, skip our first note 194 | if (!second || (added > 0)) 195 | index_map_.add(pointers[position]); 196 | added += 1; 197 | pointers[position] -= (1-2*position); 198 | position = 1-position; 199 | DBGLOG(" roa: p0 = ", pointers[0], "; p1 = ", pointers[1], "; position = ", position) 200 | } 201 | 202 | } 203 | }; 204 | -------------------------------------------------------------------------------- /Source/Processor/setup.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #include "../PluginProcessor.hpp" 14 | #include "../PluginEditor.hpp" 15 | #include "../Starp.hpp" 16 | 17 | #include "version.hpp" 18 | 19 | #if SF_DEBUG 20 | std::unique_ptr dbgout = 21 | std::unique_ptr(juce::FileLogger::createDateStampedLogger("SolidFuel/Arp", "ArpLogFile", ".txt", "---- " + PLUGIN_VERSION + " (" + GIT_HASH +")")); 22 | #endif 23 | 24 | 25 | PluginProcessor::PluginProcessor() : /* juce::AudioProcessor(getDefaultProperties()), */ 26 | parameters_(*this) { 27 | 28 | 29 | 30 | algo_listener_.onChange = [this](juce::Value &) { algo_changed_ = true; }; 31 | parameters_.algorithm_index.addListener(&algo_listener_); 32 | 33 | DBGLOG("Finished Processor Constructor") 34 | 35 | } 36 | 37 | PluginProcessor::~PluginProcessor() { 38 | 39 | DBGLOG("PluginProcessor destructor called") 40 | } 41 | 42 | bool PluginProcessor::isMidiEffect() const { 43 | return host_type.isReaper(); 44 | } 45 | 46 | juce::AudioProcessor::BusesProperties PluginProcessor::getDefaultProperties() { 47 | auto retval = BusesProperties(); 48 | 49 | if (! host_type.isReaper()) { 50 | // Unless we are on reaper, act as if we are a synth. 51 | retval = retval.withOutput("Output", juce::AudioChannelSet::stereo(), true); 52 | } 53 | 54 | return retval; 55 | } 56 | 57 | 58 | juce::AudioProcessorEditor* PluginProcessor::createEditor() { 59 | 60 | DBGLOG("------- Setting Up Editor -----------"); 61 | return new PluginEditor (*this); 62 | } 63 | 64 | 65 | //============================================================================ 66 | // Serialize Parameters for the host to save for us. 67 | // 68 | 69 | constexpr int CURRENT_STATE_VERSION = 2; 70 | const juce::String XML_TOP_TAG = "Starp-Preset"; 71 | 72 | void PluginProcessor::getStateInformation (juce::MemoryBlock& destData) { 73 | 74 | DBGLOG("GET STATE called"); 75 | 76 | auto xml = std::make_unique(XML_TOP_TAG); 77 | xml->setAttribute("version", CURRENT_STATE_VERSION); 78 | 79 | DBGLOG(" Created Top") 80 | 81 | //-------------------------------------- 82 | auto state = parameters_.apvts->copyState(); 83 | auto apvts_xml =state.createXml(); 84 | // createXml gives back a unqiue_ptr. So we need to unwrap it. 85 | xml->addChildElement(apvts_xml.release()); 86 | DBGLOG(" Wrote ValueTree") 87 | 88 | //-------------------------------------- 89 | xml->setAttribute("key", juce::String{parameters_.get_random_seed()}); 90 | xml->setAttribute("algorithm", parameters_.get_algo_index()); 91 | DBGLOG(" Wrote attributes") 92 | 93 | //-------------------------------------- 94 | auto *child = xml->createNewChildElement("RandomParameters"); 95 | child->setAttribute("replace", parameters_.random_parameters.get_replace()); 96 | DBGLOG(" Wrote RandomParameters") 97 | 98 | //-------------------------------------- 99 | child = xml->createNewChildElement("LinearParameters"); 100 | child->setAttribute("direction", parameters_.linear_parameters.get_direction()); 101 | child->setAttribute("zigzag", parameters_.linear_parameters.get_zigzag()); 102 | child->setAttribute("restart", parameters_.linear_parameters.get_restart()); 103 | DBGLOG(" Wrote LinearParameters") 104 | 105 | //-------------------------------------- 106 | child = xml->createNewChildElement("SpiralParameters"); 107 | child->setAttribute("direction", parameters_.spiral_parameters.get_direction()); 108 | child->setAttribute("start_position", parameters_.spiral_parameters.get_start_position()); 109 | DBGLOG(" Wrote SpiralParameters") 110 | 111 | //-------------------------------------- 112 | DBGLOG("XML out =", xml->toString()); 113 | copyXmlToBinary(*xml, destData); 114 | DBGLOG(" Done") 115 | 116 | } 117 | 118 | //============================================================================ 119 | void PluginProcessor::parseCurrentXml(const juce::XmlElement * elem) { 120 | 121 | DBGLOG("PluginProcessor::parseCurrentXml called") 122 | 123 | auto *child = elem->getChildByName(parameters_.apvts->state.getType()); 124 | if (child) { 125 | parameters_.apvts->replaceState(juce::ValueTree::fromXml(*child)); 126 | } 127 | 128 | DBGLOG(" -- apvts done") 129 | 130 | child = elem->getChildByName("LinearParameters"); 131 | if (child) { 132 | parameters_.linear_parameters.direction = 133 | child->getIntAttribute("direction", LinearParameters::Direction::Up); 134 | parameters_.linear_parameters.zigzag = 135 | child->getBoolAttribute("zigzag", false); 136 | parameters_.linear_parameters.restart = 137 | child->getBoolAttribute("restart", false); 138 | } 139 | 140 | DBGLOG(" -- linear done") 141 | 142 | child = elem->getChildByName("SpiralParameters"); 143 | if (child) { 144 | parameters_.spiral_parameters.direction = 145 | child->getIntAttribute("direction", SpiralParameters::Direction::In); 146 | parameters_.spiral_parameters.start_position = 147 | child->getIntAttribute("start_position", SpiralParameters::StartPosition::Top); 148 | } 149 | 150 | DBGLOG(" -- spiral done") 151 | 152 | child = elem->getChildByName("RandomParameters"); 153 | if (child) { 154 | parameters_.random_parameters.replace = 155 | child->getBoolAttribute("replace", false); 156 | } 157 | 158 | DBGLOG(" -- random done") 159 | 160 | // These 2 for historical reasons reside on the main tag. 161 | parameters_.random_parameters.seed_value = 162 | elem->getStringAttribute("key", juce::String{parameters_.get_random_seed()}) 163 | .getLargeIntValue(); 164 | parameters_.algorithm_index = 165 | elem->getIntAttribute("algorithm", Algorithm::Random); 166 | 167 | DBGLOG(" -- others done") 168 | 169 | 170 | } 171 | 172 | //============================================================================ 173 | void PluginProcessor::parseOriginalXml(const juce::XmlElement * xml) { 174 | 175 | DBGLOG("PluginProcessor::parseOriginalXml called") 176 | 177 | if (xml->hasTagName(parameters_.apvts->state.getType())) { 178 | parameters_.apvts->replaceState(juce::ValueTree::fromXml(*xml)); 179 | 180 | auto child = xml->getChildByName("LinearParameters"); 181 | if (child) { 182 | parameters_.linear_parameters.direction = 183 | child->getIntAttribute("direction", LinearParameters::Direction::Up); 184 | parameters_.linear_parameters.zigzag = 185 | child->getBoolAttribute("zigzag", false); 186 | } 187 | 188 | parameters_.random_parameters.seed_value = 189 | xml->getStringAttribute("key", juce::String{parameters_.get_random_seed()}) 190 | .getLargeIntValue(); 191 | 192 | auto algo = xml->getIntAttribute("algorithm", Algorithm::Random); 193 | DBGLOG(" input algo = ", algo) 194 | if (algo == Algorithm::AlgDown ) { 195 | algo = Algorithm::Linear; 196 | parameters_.linear_parameters.direction = LinearParameters::Direction::Down; 197 | parameters_.linear_parameters.zigzag = false; 198 | } else if (algo == Algorithm::AlgUp ) { 199 | algo = Algorithm::Linear; 200 | parameters_.linear_parameters.direction = LinearParameters::Direction::Up; 201 | parameters_.linear_parameters.zigzag = false; 202 | } 203 | parameters_.algorithm_index.setValue(algo); 204 | DBGLOG(" final algo = ", parameters_.get_algo_index()) 205 | } 206 | } 207 | 208 | //============================================================================ 209 | // Read Serialize Parameters from the host and set our state. 210 | // 211 | void PluginProcessor::setStateInformation (const void* data, int sizeInBytes) { 212 | DBGLOG("SET STATE called"); 213 | 214 | auto xml = getXmlFromBinary(data, sizeInBytes); 215 | 216 | if (xml) { 217 | DBGLOG("XML in =", xml->toString()); 218 | 219 | if (xml->hasTagName(XML_TOP_TAG)) { 220 | int version = xml->getIntAttribute("version"); 221 | if (version == CURRENT_STATE_VERSION) { 222 | parseCurrentXml(xml.get()); 223 | } else { 224 | jassert(false); 225 | } 226 | 227 | } else { 228 | parseOriginalXml(xml.get()); 229 | } 230 | 231 | } else { 232 | DBGLOG(" NO XML decoded") 233 | } 234 | } 235 | 236 | //============================================================================ 237 | // This creates new instances of the plugin.. 238 | juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() 239 | { 240 | return new PluginProcessor(); 241 | } 242 | -------------------------------------------------------------------------------- /Source/EditorComponent/PropertyComponent.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #include "PropertyComponent.hpp" 14 | #include "../Starp.hpp" 15 | 16 | 17 | 18 | 19 | PropertyComponent::PropertyComponent (ProcessorParameters *params) : 20 | params_(params) 21 | { 22 | 23 | DBGLOG("Setting up PropertyComponent"); 24 | auto apvts = params->apvts.get(); 25 | 26 | //============================================== 27 | 28 | // --------- Speed ------------ 29 | 30 | 31 | speedNoteSlider_.setTooltip("How often a note will (possibly) be generated as a note value."); 32 | speedNoteAttachment_.reset (new SliderAttachment (*apvts, ProcessorParameters::SPEED_NOTE_ID, speedNoteSlider_)); 33 | speedComponent_.add(speedNoteSlider_); 34 | speedBarSlider_.setTooltip("How often a note will (possibly) be generated (in notes per bar)."); 35 | speedBarAttachment_.reset (new SliderAttachment (*apvts, ProcessorParameters::SPEED_BAR_ID, speedBarSlider_)); 36 | speedComponent_.add(speedBarSlider_); 37 | speedMSecSlider_.setTooltip("How often a note will (possibly) be generated in milliseconds"); 38 | speedMSecAttachment_.reset (new SliderAttachment (*apvts, ProcessorParameters::SPEED_MSEC_ID, speedMSecSlider_)); 39 | speedComponent_.add(speedMSecSlider_); 40 | 41 | 42 | speed_type_value_.referTo(params_->apvts->getParameterAsValue(ProcessorParameters::SPEED_TYPE_ID)); 43 | speed_type_listener_.onChange = [this](juce::Value &v) { 44 | update_speed_type(SpeedType(int(v.getValue()))); 45 | }; 46 | update_speed_type(SpeedType(int(speed_type_value_.getValue()))); 47 | speed_type_value_.addListener(&speed_type_listener_); 48 | 49 | speedType_.setValue(speed_type_value_); 50 | speedType_.refresh(); 51 | 52 | speedBox_.add(speedType_, 0, 8); 53 | speedBox_.add(speedComponent_); 54 | speedBox_.setText("Speed"); 55 | addAndMakeVisible(speedBox_); 56 | 57 | 58 | // --------- Probability ------------ 59 | probabilityLabel_.setText ("Probability", juce::dontSendNotification); 60 | probabilityLabel_.setTooltip("Chance of a note being generated"); 61 | probabilitySlider_.setTextValueSuffix("%"); 62 | probabilitySlider_.setTooltip("Chance of a note being generated"); 63 | probabilityAttachment_.reset (new SliderAttachment (*apvts, "probability", probabilitySlider_)); 64 | 65 | addAndMakeVisible(probabilityLabel_); 66 | addAndMakeVisible(probabilitySlider_); 67 | 68 | // --------- Gate ------------ 69 | gateLabel_.setText ("Gate %", juce::dontSendNotification); 70 | gateLabel_.setTooltip("How long the note will be on as a proportion of the speed."); 71 | gateSlider_.setTextValueSuffix("%"); 72 | gateSlider_.setTooltip("How long the note will be on as a proportion of the speed."); 73 | gateAttachment_.reset (new SliderAttachment (*apvts, "gate", gateSlider_)); 74 | 75 | gateBox_.addAndMakeVisible(gateLabel_); 76 | gateBox_.addAndMakeVisible(gateSlider_); 77 | 78 | // --------- Gate Range ------------ 79 | gateRangeLabel_.setText ("Range", juce::dontSendNotification); 80 | gateRangeLabel_.setTooltip("Range in variance (+/-) for the gate"); 81 | gateRangeSlider_.setTextValueSuffix("%"); 82 | gateRangeSlider_.setTooltip("Range in variance (+/-) for the gate"); 83 | gateRangeAttachment_.reset (new SliderAttachment (*apvts, "gate_range", gateRangeSlider_)); 84 | 85 | gateRangeBox_.addAndMakeVisible(gateRangeLabel_); 86 | gateRangeBox_.addAndMakeVisible(gateRangeSlider_); 87 | 88 | // --------- Velocity ------------ 89 | veloLabel_.setText ("Velocity", juce::dontSendNotification); 90 | veloLabel_.setTooltip("MIDI velocity of the generated note"); 91 | veloSlider_.setTooltip("MIDI velocity of the generated note"); 92 | veloAttachment_.reset (new SliderAttachment (*apvts, "velocity", veloSlider_)); 93 | 94 | veloBox_.addAndMakeVisible(veloLabel_); 95 | veloBox_.addAndMakeVisible(veloSlider_); 96 | 97 | // --------- Velocity Range ------------ 98 | veloRangeLabel_.setText ("Range", juce::dontSendNotification); 99 | veloRangeLabel_.setTooltip("Range of variance (+/-) of the velocity"); 100 | veloRangeSlider_.setTooltip("Range of variance (+/-) of the velocity"); 101 | veloRangeAttachment_.reset (new SliderAttachment (*apvts, "velocity_range", veloRangeSlider_)); 102 | 103 | veloRangeBox_.addAndMakeVisible (veloRangeLabel_); 104 | veloRangeBox_.addAndMakeVisible(veloRangeSlider_); 105 | 106 | // --------- Advance ------------ 107 | advanceLabel_.setText ("Advance", juce::dontSendNotification); 108 | advanceLabel_.setTooltip("Variance in the timing - this sets the how much it might be early"); 109 | 110 | advanceSlider_.setTooltip("Variance in the timing - this sets the how much it might be early"); 111 | advanceAttachment_.reset (new SliderAttachment (*apvts, "timing_advance", advanceSlider_)); 112 | 113 | advanceBox_.addAndMakeVisible (advanceLabel_); 114 | advanceBox_.addAndMakeVisible(advanceSlider_); 115 | 116 | // --------- Delay ------------ 117 | delayLabel_.setText ("Delay", juce::dontSendNotification); 118 | delayLabel_.setTooltip("Variance in the timing - this sets the how much it might be late"); 119 | 120 | delaySlider_.setTooltip("Variance in the timing - this sets the how much it might be late"); 121 | delayAttachment_.reset (new SliderAttachment (*apvts, "timing_delay", delaySlider_)); 122 | 123 | delayBox_.addAndMakeVisible (delayLabel_); 124 | delayBox_.addAndMakeVisible(delaySlider_); 125 | 126 | // --------- Gate Group ------------ 127 | gateGroup_.addAndMakeVisible(gateBox_); 128 | gateGroup_.addAndMakeVisible(gateRangeBox_); 129 | gateGroup_.setText("Gate"); 130 | 131 | addAndMakeVisible(gateGroup_); 132 | 133 | // --------- Velocity Group ------------ 134 | veloGroup_.addAndMakeVisible(veloBox_); 135 | veloGroup_.addAndMakeVisible(veloRangeBox_); 136 | veloGroup_.setText("Velocity"); 137 | 138 | addAndMakeVisible(veloGroup_); 139 | 140 | // --------- Timing Group ------------ 141 | timingGroup_.addAndMakeVisible(advanceBox_); 142 | timingGroup_.addAndMakeVisible(delayBox_); 143 | timingGroup_.setText("Timing"); 144 | 145 | addAndMakeVisible(timingGroup_); 146 | 147 | } 148 | 149 | //============================================================================== 150 | void PropertyComponent::update_speed_type(SpeedType sp) { 151 | 152 | DBGLOG("PropertyComponent::update_speed_type called = ", sp) 153 | 154 | speedComponent_.setActiveIndex(sp); 155 | 156 | speedType_.refresh(); 157 | 158 | } 159 | 160 | //============================================================================== 161 | void PropertyComponent::paint (juce::Graphics& g) { 162 | g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId)); 163 | 164 | } 165 | 166 | //============================================================================== 167 | void PropertyComponent::resized() { 168 | 169 | DBGLOG("PropertyComponent::resized called") 170 | 171 | juce::Grid grid; 172 | 173 | using Track = juce::Grid::TrackInfo; 174 | using Fr = juce::Grid::Fr; 175 | using GridItem = juce::GridItem; 176 | 177 | juce::Array column_layout = { Track (Fr (10)), Track(Fr(50)) }; 178 | juce::Array sub_group_layout = { Track (Fr (1)), Track(Fr(1)) }; 179 | 180 | grid.alignItems = juce::Grid::AlignItems::center; 181 | grid.justifyContent = juce::Grid::JustifyContent::start; 182 | grid.justifyItems = juce::Grid::JustifyItems::start; 183 | 184 | grid.templateColumns = column_layout; 185 | 186 | 187 | //---------------------------------------- 188 | 189 | grid.templateRows.add(Track (Fr (10))); 190 | grid.items.add(GridItem(probabilityLabel_)); 191 | grid.items.add(GridItem(probabilitySlider_)); 192 | 193 | speedBox_.layoutTemplate = column_layout; 194 | 195 | grid.templateRows.add(Track (Fr (13))); 196 | grid.items.add(GridItem(speedBox_).withArea(GridItem::Span(1), GridItem::Span(2))); 197 | 198 | gateBox_.layoutTemplate = column_layout; 199 | gateRangeBox_.layoutTemplate = column_layout; 200 | gateGroup_.layoutTemplate = sub_group_layout; 201 | 202 | grid.templateRows.add(Track (Fr (23))); 203 | grid.items.add(GridItem(gateGroup_).withArea(GridItem::Span(1), GridItem::Span(2))); 204 | 205 | veloBox_.layoutTemplate = column_layout; 206 | veloRangeBox_.layoutTemplate = column_layout; 207 | veloGroup_.layoutTemplate = sub_group_layout; 208 | 209 | grid.templateRows.add(Track (Fr (23))); 210 | grid.items.add(GridItem(veloGroup_).withArea(GridItem::Span(1), GridItem::Span(2))); 211 | 212 | advanceBox_.layoutTemplate = column_layout; 213 | delayBox_.layoutTemplate = column_layout; 214 | timingGroup_.layoutTemplate = sub_group_layout; 215 | 216 | grid.templateRows.add(Track (Fr (23))); 217 | grid.items.add(GridItem(timingGroup_).withArea(GridItem::Span(1), GridItem::Span(2))); 218 | 219 | grid.performLayout (getLocalBounds()); 220 | 221 | } 222 | 223 | -------------------------------------------------------------------------------- /CMake/GetGitRevisionDescription.cmake: -------------------------------------------------------------------------------- 1 | # - Returns a version string from Git 2 | # 3 | # These functions force a re-configure on each git commit so that you can 4 | # trust the values of the variables in your build system. 5 | # 6 | # get_git_head_revision( [ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR]) 7 | # 8 | # Returns the refspec and sha hash of the current head revision 9 | # 10 | # git_describe( [ ...]) 11 | # 12 | # Returns the results of git describe on the source tree, and adjusting 13 | # the output so that it tests false if an error occurs. 14 | # 15 | # git_describe_working_tree( [ ...]) 16 | # 17 | # Returns the results of git describe on the working tree (--dirty option), 18 | # and adjusting the output so that it tests false if an error occurs. 19 | # 20 | # git_get_exact_tag( [ ...]) 21 | # 22 | # Returns the results of git describe --exact-match on the source tree, 23 | # and adjusting the output so that it tests false if there was no exact 24 | # matching tag. 25 | # 26 | # git_local_changes() 27 | # 28 | # Returns either "CLEAN" or "DIRTY" with respect to uncommitted changes. 29 | # Uses the return code of "git diff-index --quiet HEAD --". 30 | # Does not regard untracked files. 31 | # 32 | # Requires CMake 2.6 or newer (uses the 'function' command) 33 | # 34 | # Original Author: 35 | # 2009-2020 Ryan Pavlik 36 | # http://academic.cleardefinition.com 37 | # 38 | # Copyright 2009-2013, Iowa State University. 39 | # Copyright 2013-2020, Ryan Pavlik 40 | # Copyright 2013-2020, Contributors 41 | # SPDX-License-Identifier: BSL-1.0 42 | # Distributed under the Boost Software License, Version 1.0. 43 | # (See accompanying file LICENSE_1_0.txt or copy at 44 | # http://www.boost.org/LICENSE_1_0.txt) 45 | 46 | if(__get_git_revision_description) 47 | return() 48 | endif() 49 | set(__get_git_revision_description YES) 50 | 51 | # We must run the following at "include" time, not at function call time, 52 | # to find the path to this module rather than the path to a calling list file 53 | get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH) 54 | 55 | # Function _git_find_closest_git_dir finds the next closest .git directory 56 | # that is part of any directory in the path defined by _start_dir. 57 | # The result is returned in the parent scope variable whose name is passed 58 | # as variable _git_dir_var. If no .git directory can be found, the 59 | # function returns an empty string via _git_dir_var. 60 | # 61 | # Example: Given a path C:/bla/foo/bar and assuming C:/bla/.git exists and 62 | # neither foo nor bar contain a file/directory .git. This wil return 63 | # C:/bla/.git 64 | # 65 | function(_git_find_closest_git_dir _start_dir _git_dir_var) 66 | set(cur_dir "${_start_dir}") 67 | set(git_dir "${_start_dir}/.git") 68 | while(NOT EXISTS "${git_dir}") 69 | # .git dir not found, search parent directories 70 | set(git_previous_parent "${cur_dir}") 71 | get_filename_component(cur_dir "${cur_dir}" DIRECTORY) 72 | if(cur_dir STREQUAL git_previous_parent) 73 | # We have reached the root directory, we are not in git 74 | set(${_git_dir_var} 75 | "" 76 | PARENT_SCOPE) 77 | return() 78 | endif() 79 | set(git_dir "${cur_dir}/.git") 80 | endwhile() 81 | set(${_git_dir_var} 82 | "${git_dir}" 83 | PARENT_SCOPE) 84 | endfunction() 85 | 86 | function(get_git_head_revision _refspecvar _hashvar) 87 | _git_find_closest_git_dir("${CMAKE_CURRENT_SOURCE_DIR}" GIT_DIR) 88 | 89 | if("${ARGN}" STREQUAL "ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR") 90 | set(ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR TRUE) 91 | else() 92 | set(ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR FALSE) 93 | endif() 94 | if(NOT "${GIT_DIR}" STREQUAL "") 95 | file(RELATIVE_PATH _relative_to_source_dir "${CMAKE_SOURCE_DIR}" 96 | "${GIT_DIR}") 97 | if("${_relative_to_source_dir}" MATCHES "[.][.]" 98 | AND NOT ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR) 99 | # We've gone above the CMake root dir. 100 | set(GIT_DIR "") 101 | endif() 102 | endif() 103 | if("${GIT_DIR}" STREQUAL "") 104 | set(${_refspecvar} 105 | "GITDIR-NOTFOUND" 106 | PARENT_SCOPE) 107 | set(${_hashvar} 108 | "GITDIR-NOTFOUND" 109 | PARENT_SCOPE) 110 | return() 111 | endif() 112 | 113 | # Check if the current source dir is a git submodule or a worktree. 114 | # In both cases .git is a file instead of a directory. 115 | # 116 | if(NOT IS_DIRECTORY ${GIT_DIR}) 117 | # The following git command will return a non empty string that 118 | # points to the super project working tree if the current 119 | # source dir is inside a git submodule. 120 | # Otherwise the command will return an empty string. 121 | # 122 | execute_process( 123 | COMMAND "${GIT_EXECUTABLE}" rev-parse 124 | --show-superproject-working-tree 125 | WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" 126 | OUTPUT_VARIABLE out 127 | ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) 128 | if(NOT "${out}" STREQUAL "") 129 | # If out is empty, GIT_DIR/CMAKE_CURRENT_SOURCE_DIR is in a submodule 130 | file(READ ${GIT_DIR} submodule) 131 | string(REGEX REPLACE "gitdir: (.*)$" "\\1" GIT_DIR_RELATIVE 132 | ${submodule}) 133 | string(STRIP ${GIT_DIR_RELATIVE} GIT_DIR_RELATIVE) 134 | get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH) 135 | get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} 136 | ABSOLUTE) 137 | set(HEAD_SOURCE_FILE "${GIT_DIR}/HEAD") 138 | else() 139 | # GIT_DIR/CMAKE_CURRENT_SOURCE_DIR is in a worktree 140 | file(READ ${GIT_DIR} worktree_ref) 141 | # The .git directory contains a path to the worktree information directory 142 | # inside the parent git repo of the worktree. 143 | # 144 | string(REGEX REPLACE "gitdir: (.*)$" "\\1" git_worktree_dir 145 | ${worktree_ref}) 146 | string(STRIP ${git_worktree_dir} git_worktree_dir) 147 | _git_find_closest_git_dir("${git_worktree_dir}" GIT_DIR) 148 | set(HEAD_SOURCE_FILE "${git_worktree_dir}/HEAD") 149 | endif() 150 | else() 151 | set(HEAD_SOURCE_FILE "${GIT_DIR}/HEAD") 152 | endif() 153 | set(GIT_DATA "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data") 154 | if(NOT EXISTS "${GIT_DATA}") 155 | file(MAKE_DIRECTORY "${GIT_DATA}") 156 | endif() 157 | 158 | if(NOT EXISTS "${HEAD_SOURCE_FILE}") 159 | return() 160 | endif() 161 | set(HEAD_FILE "${GIT_DATA}/HEAD") 162 | configure_file("${HEAD_SOURCE_FILE}" "${HEAD_FILE}" COPYONLY) 163 | 164 | configure_file("${_gitdescmoddir}/GetGitRevisionDescription.cmake.in" 165 | "${GIT_DATA}/grabRef.cmake" @ONLY) 166 | include("${GIT_DATA}/grabRef.cmake") 167 | 168 | set(${_refspecvar} 169 | "${HEAD_REF}" 170 | PARENT_SCOPE) 171 | set(${_hashvar} 172 | "${HEAD_HASH}" 173 | PARENT_SCOPE) 174 | endfunction() 175 | 176 | function(git_describe _var) 177 | if(NOT GIT_FOUND) 178 | find_package(Git QUIET) 179 | endif() 180 | get_git_head_revision(refspec hash) 181 | if(NOT GIT_FOUND) 182 | set(${_var} 183 | "GIT-NOTFOUND" 184 | PARENT_SCOPE) 185 | return() 186 | endif() 187 | if(NOT hash) 188 | set(${_var} 189 | "HEAD-HASH-NOTFOUND" 190 | PARENT_SCOPE) 191 | return() 192 | endif() 193 | 194 | # TODO sanitize 195 | #if((${ARGN}" MATCHES "&&") OR 196 | # (ARGN MATCHES "||") OR 197 | # (ARGN MATCHES "\\;")) 198 | # message("Please report the following error to the project!") 199 | # message(FATAL_ERROR "Looks like someone's doing something nefarious with git_describe! Passed arguments ${ARGN}") 200 | #endif() 201 | 202 | #message(STATUS "Arguments to execute_process: ${ARGN}") 203 | 204 | execute_process( 205 | COMMAND "${GIT_EXECUTABLE}" describe --tags --always ${hash} ${ARGN} 206 | WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" 207 | RESULT_VARIABLE res 208 | OUTPUT_VARIABLE out 209 | ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) 210 | if(NOT res EQUAL 0) 211 | set(out "${out}-${res}-NOTFOUND") 212 | endif() 213 | 214 | set(${_var} 215 | "${out}" 216 | PARENT_SCOPE) 217 | endfunction() 218 | 219 | function(git_describe_working_tree _var) 220 | if(NOT GIT_FOUND) 221 | find_package(Git QUIET) 222 | endif() 223 | if(NOT GIT_FOUND) 224 | set(${_var} 225 | "GIT-NOTFOUND" 226 | PARENT_SCOPE) 227 | return() 228 | endif() 229 | 230 | execute_process( 231 | COMMAND "${GIT_EXECUTABLE}" describe --dirty ${ARGN} 232 | WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" 233 | RESULT_VARIABLE res 234 | OUTPUT_VARIABLE out 235 | ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) 236 | if(NOT res EQUAL 0) 237 | set(out "${out}-${res}-NOTFOUND") 238 | endif() 239 | 240 | set(${_var} 241 | "${out}" 242 | PARENT_SCOPE) 243 | endfunction() 244 | 245 | function(git_get_exact_tag _var) 246 | git_describe(out --exact-match ${ARGN}) 247 | set(${_var} 248 | "${out}" 249 | PARENT_SCOPE) 250 | endfunction() 251 | 252 | function(git_local_changes _var) 253 | if(NOT GIT_FOUND) 254 | find_package(Git QUIET) 255 | endif() 256 | get_git_head_revision(refspec hash) 257 | if(NOT GIT_FOUND) 258 | set(${_var} 259 | "GIT-NOTFOUND" 260 | PARENT_SCOPE) 261 | return() 262 | endif() 263 | if(NOT hash) 264 | set(${_var} 265 | "HEAD-HASH-NOTFOUND" 266 | PARENT_SCOPE) 267 | return() 268 | endif() 269 | 270 | execute_process( 271 | COMMAND "${GIT_EXECUTABLE}" diff-index --quiet HEAD -- 272 | WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" 273 | RESULT_VARIABLE res 274 | OUTPUT_VARIABLE out 275 | ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) 276 | if(res EQUAL 0) 277 | set(${_var} 278 | "CLEAN" 279 | PARENT_SCOPE) 280 | else() 281 | set(${_var} 282 | "DIRTY" 283 | PARENT_SCOPE) 284 | endif() 285 | endfunction() -------------------------------------------------------------------------------- /docs/USER_MANUAL.md: -------------------------------------------------------------------------------- 1 | # solidArp _V0.3.0_ User Manual 2 | 3 | **solidArp** is a powerful arppegiator that is a 4 | little different from others. 5 | 6 | In Random mode _solidArp_ will make sure that the 7 | sequence generate - while indeed random - is stable. 8 | If the midi data doesn't change during successive 9 | playbacks, the sequences generated will not change. 10 | 11 | ## DAW Setup 12 | 13 | If a DAW does not appear below, it has not been tested. 14 | 15 | ### Reaper 6,7 16 | 17 | Put it in the Track FX list ahead of the synth. 18 | 19 | ### Ableton Live 11 20 | 21 | Live does not recognize MIDI effects. So, to use this, you will need two tracks. 22 | 23 | 1. The track with the MIDI and _Starp_. 24 | 1. The track with the synth. 25 | 26 | Set the input of the Synth track to be the track with _Starp_ . In the second 27 | drop down, choose _Starp_ itself (not Pre FX or Post Fx) as the input. 28 | 29 | ![Live Setup Example](Live-Setup.png) 30 | 31 | ### Studio One 6 32 | 33 | Studio One does not allow you to use 3rd party MIDI effects in the Note FX 34 | slots. Set up is similar to Ableton Live 35 | 36 | _Note_ Record Arm must be on or the MIDI will not reach the synth. Yes, this is 37 | painful for no good reason. 38 | [Go Vote](https://answers.presonus.com/43595/add-support-for-third-party-note-fx) 39 | to change this. 40 | 41 | ![Live Setup Example](StudioOne-setup.png) 42 | 43 | ### FL Studio 21 44 | 45 | FL does not recognize MIDI effects. The easiest way to set this up is using 46 | patcher. 47 | 48 | 1. Add a patcher to the instrument bank with whatever synth/instrument you want. 49 | 1. Add _Starp_ to the patcher and hook it up (it should look like the below). 50 | **NOTE** Add it from the synth subsection of the plugin menu, not the effects 51 | subsection. 52 | 1. Double Click on the _Starp_ instance and the use the cogwheel in the top-left 53 | to set the midi output port. Exact port number does not matter. 54 | 55 | ![FL Studio Patcher example](starp-patcher.png) 56 | 57 | ![FL Studio Midi setting for Starp](Starp-patcher-settings.png) 58 | 59 | ### Cubase 12 60 | 61 | Similar to the Ableton Live set up, you will need two tracks. Be sure to route 62 | the midi from the track with _Starp_ on it to the instrument track (See below). 63 | 64 | _Note_ Record Arm must be on or the MIDI will not reach the synth. 65 | 66 | ![Cubase Midi Setup](cubase-channel-setup.png) 67 | 68 | ## User Interface 69 | 70 | ### Header 71 | 72 | ![solidArp's Header](solidArp-header.png) 73 | 74 | The `menu` button on the left contains the `about` menu option to display 75 | version, copyright, and other information about the software. 76 | 77 | ### Algorithm 78 | 79 | The algorithm section consists of two rows. The top row allows you to pick the 80 | algorithm. The bottom row contains options specific to the algorithm. 81 | 82 | **NOTE**: None of the settings in this section are automatable in the host and 83 | will not show up in the parameters list. 84 | 85 | ### Random Algorithm 86 | 87 | This algorithm is the reason the plug-in exists. Most random arpeggiators will 88 | give you a different sequence of notes each tme you play through the track. 89 | _solidArp_ will give you the exact same (random) sequence each time you play - 90 | assuming there is no changes to e.g. the MIDI data or BPM, etc. 91 | 92 | The algorithm does its best to make sure that the same note is not played twice 93 | in a row. 94 | 95 | ![Random Algorithm Settings](Random-algorithm.png) 96 | 97 | #### Seed 98 | 99 | This is the value used to initialize the random number generator that selects 100 | tones. It is saved as a part of the configuration when you save your project in 101 | your DAW. It is displayed as information only. 102 | 103 | #### New Seed Button 104 | 105 | When pressed, this button will generate a new seed. 106 | 107 | _Note_ : There is currently no way to return to a previous seed. So be sure to 108 | save your project if you want to be able to get back to the previous seed. 109 | 110 | #### Replace 111 | 112 | By default, the random algorithm will not play a note that it has already played 113 | until all currently sounding notes have been played (choice without 114 | replacement). 115 | 116 | When the options is on (check mark in the box) this will no longer be the case. 117 | Each time it must choose a note, it will choose from the full range of notes 118 | currently being played (choice with replacement). 119 | 120 | In either case, the set of notes it will choose from will be refreshed every 121 | time the midi data changes. 122 | 123 | ### Linear Algorithm 124 | 125 | ![Linear Algorithm Settings](Linear-algorithm.png) 126 | 127 | #### Up / Down 128 | 129 | These button set the primary direction for the arpeggiation. When `Up` is 130 | selected, the arp will start at the lowest note and work up. If `Down` is 131 | selected, the arp will start at the highest note and work down. 132 | 133 | #### Zigzag 134 | 135 | When selected, the arp will go both up **and** down. The starting direction is 136 | set by the Up/Down radio buttons. 137 | 138 | The zigzag does not repeat the note in the middle. So, if `Up` and `Zigzag` are 139 | chosen, the highest note will not be repeated in the middle of the sections. 140 | 141 | #### Restart 142 | 143 | By default, the sequence runs continuously in the background. If there are no 144 | notes being played, then the plugin creates no midi data, but the sequence is 145 | still running. 146 | 147 | When `Restart` is selected, the sequence will restart every time the midi notes 148 | change. 149 | 150 | ### Spiral Algorithm 151 | 152 | ![Spiral Algorithm Settings](Spiral-algorithm.png) 153 | 154 | #### Primary Note Selection 155 | 156 | These radio buttons set whether the top or bottom note is the first (for `In`) or 157 | last (for `Out`) note in the sequence. For the `InOut` and `OutIn` sequences, 158 | these set both the starting and the ending note. 159 | 160 | #### Sequence Selection 161 | 162 | These radio buttons set which of 4 sequences to generate. 163 | 164 | **In** The sequence starts on the Primary Note and "spirals in" to the center. 165 | 166 | **Out** The sequence starts on the center note and "spirals out" to the Primary 167 | Note. 168 | 169 | **InOut** The `In` and `Out` sequences are performed back-to-back. _note_: The 170 | center not is **not** repeated - but the primary note will be as the sequence 171 | restarts. 172 | 173 | **OutIn** The `Out` and `In` sequences are performed back-to-back. _note_: The 174 | center not is **not** repeated - but the primary note will be as the sequence 175 | restarts. 176 | 177 | ## Parameters 178 | 179 | All setting in this section are automatable by the host and can be controlled 180 | via a MIDI controller. See the Appendix for the names of the parameters and how 181 | they map to controls in the plug-in. 182 | 183 | Each of the sliders in this section work the same way. You can change the value 184 | by 185 | - Grabbing the slider knob and moving it along the track. 186 | - Clicking in the track to move the slider knob to that location. 187 | - Double click in the track to return the slider knob to the 188 | default value. 189 | - Clicking on the box that shows the value and typing in a new value. 190 | - Scrolling the mouse wheel while the mouse pointer is on the slider track. 191 | 192 | For those sliders that have continuously variable values, a "Fine Adjustment" 193 | mode is available by holding down `Cmd/Ctrl` while dragging the slider knob. 194 | 195 | ### Probability 196 | 197 | Sets the chance that a note will be played. Ranges from 0% (never) to 100% 198 | (always). The default setting is 100%. 199 | 200 | Note that sequences are advanced even if a note is not played. 201 | 202 | ### Speed 203 | 204 | There are three ways to specify the speed of the arpeggiation. 205 | 206 | - Note value 207 | - Bar division 208 | - Millisecond 209 | 210 | #### Note Value 211 | 212 | ![Speed Note Values](Speed-note.png) 213 | 214 | Speed is specified as a Note Value - `1/8` means eighth note, etc. The following 215 | suffixes are used to modify the base value: 216 | 217 | | Suffix | Meaning | 218 | | ------ | -------------------------------------------- | 219 | | d | Dotted - the value is 150% of the base value | 220 | | t | Triplet - the value is 66% of the base value | 221 | | q | Quintuplet - the value 80% of the base value | 222 | 223 | Faster timings are on the left and slower on the right. 224 | 225 | _solidArp_ uses data from both the BPM of the track and the time signature to 226 | compute the actual length. 227 | 228 | For the whole note values ('1/1', '1/1t', '1/1q') the length of the note is 229 | equivalent to 4 quarters notes _not_ a bar. So, if the time signature is 3/4, 230 | then `1/1` will last for longer than a measure. 231 | 232 | #### Bar Divisions 233 | 234 | ![Speed Bar Divisions](Speed-bar.png) 235 | 236 | Speed is specified as the number of notes to place in a measure. 237 | 238 | Faster timings are on the left and slower on the right. 239 | 240 | Fine Adjustment mode is available but rather pointless. 241 | 242 | _solidArp_ uses data from both the BPM of the track and the time signature to 243 | compute the actual length. 244 | 245 | As an example, if you are in 3/4 time signature and select `4`, there will be 4 246 | notes generated during a single measure and each note will last the same as a 247 | dotted eighth note. 248 | 249 | The timing slots in this mode always start at the beginning of the measure as 250 | indicated by the DAW. If you select `1` but the midi information starts on beat 251 | 3 of a measure, the first note generated by the sequence will be on beat 1 of 252 | the next measure. 253 | 254 | #### Milliseconds 255 | 256 | ![Speed Milliseconds](Speed-msec.png) 257 | 258 | Speed is specified as the number of Milliseconds from the start of one note to 259 | the next. 260 | 261 | Faster timings are on the left and slower on the right. 262 | 263 | Fine Adjustment mode is available. 264 | 265 | The timing slots in this mode start from the beginning of the project and do not 266 | (necessarily) line up with the start of midi data. 267 | 268 | ### Gate 269 | 270 | ![Gate Parameter](Gate.png) 271 | 272 | Sets how long the note will be on as a percentage of the speed. This ranges 273 | from 10% up to 200%. 274 | 275 | The Range slider tells the system to randomly choose the gate amount from the 276 | interval +/- the range setting around the `Gate %`. 277 | 278 | Fine Adjustment mode is available for `Gate %`. 279 | 280 | Default value for `Gate %` is 100% and for `Range` is 0. 281 | 282 | ### Velocity 283 | 284 | ![Velocity Parameter](Velocity.png) 285 | 286 | Sets the value for the Midi velocity of the generated notes. 287 | 288 | The Range slider tells the system to randomly choose the velocity from the 289 | interval +/- the range setting around the `Velocity`. 290 | 291 | Default value of `Velocity` is 100 and for `Range` is 0. 292 | 293 | ### Timing 294 | ![Timing Parameter](Timing.png) 295 | 296 | These can be used to randomize the exact timing of the notes. 297 | 298 | `Advance` allows you to specify a range of timing so that the 299 | note possibly arrives early. This ranges from 0 to 30% of the 300 | nominal note value. 301 | 302 | `Delay` allows you to specify a range of timing so that the 303 | note possibly arrives late. This ranges from 0 to 30% of the 304 | nominal note value. 305 | 306 | The default value for both is 0. 307 | 308 | Fine Adjustment mode is available for both. 309 | 310 | 311 | ## Appendix 312 | 313 | ### Host Automatable parameters 314 | 315 | This table gives the names of the paramters that _solidArp_ makes available 316 | to the host and which control on the UI it corresponds to. 317 | 318 | |Parameter Name|UI Control| 319 | |---|---| 320 | |Advance| THe Advance slider in the Timing section| 321 | |Delay| The `Delay` slider in the Timing section| 322 | |Gate %| The `Gate %` slider| 323 | |Gate Range| The `Range` Slider in the Gate section| 324 | |Probability| The `Probability` slider| 325 | |Speed in msec| The Slider in the Speed section when `msec` is selected| 326 | |Speed in Notes| The Slider in the Speed section when `note` is selected| 327 | |Speed per Bar| The Slider in the Speed section when `bar` is selected| 328 | |Speed Type|The Combo-box in the Speed section| 329 | |Velocity| The `Velocity` slider| 330 | |Vel. Range| The `Range` slider in the Velocity section| 331 | 332 | -------------------------------------------------------------------------------- /Source/Processor/processing.cpp: -------------------------------------------------------------------------------- 1 | /**** 2 | * solidArp - Stable Random Arpeggiator Plugin 3 | * Copyright (C) 2023 Solid Fuel 4 | * This program is free software: you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License as published by the 6 | * Free Software Foundation, either version 3 of the License, or (at your 7 | * option) any later version. This program is distributed in the hope that it 8 | * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 9 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file 10 | * in the root directory. 11 | ****/ 12 | 13 | #include "../PluginProcessor.hpp" 14 | #include "../PluginEditor.hpp" 15 | #include "../Starp.hpp" 16 | 17 | #include 18 | #include 19 | 20 | 21 | 22 | //============================================================================ 23 | // PLAYED_NOTE methods 24 | //============================================================================ 25 | bool operator==(const played_note& lhs, const played_note& rhs) { 26 | return lhs.note_value == rhs.note_value; 27 | } 28 | 29 | bool operator<(const played_note& lhs, const played_note& rhs) { 30 | return lhs.note_value < rhs.note_value; 31 | } 32 | 33 | 34 | bool operator==(const schedule& lhs, const schedule& rhs){ return std::abs(lhs.start - rhs.start) < 0.000001; } 35 | bool operator<(const schedule& lhs, const schedule& rhs) { return lhs.start < rhs.start; } 36 | 37 | 38 | 39 | //============================================================================ 40 | //============================================================================ 41 | void PluginProcessor::prepareToPlay (double sampleRate, int) { 42 | 43 | DBGLOG("PREPARE called"); 44 | sample_rate_ = sampleRate; 45 | last_position_ = -1; 46 | algo_changed_ = true; 47 | 48 | // This will finesse the bypass logic so we don't reset again. 49 | last_block_call_ = juce::Time::currentTimeMillis(); 50 | 51 | DBGLOG("HiResTimer = ", juce::Time::getHighResolutionTicksPerSecond()) 52 | 53 | reset_data(); 54 | 55 | } 56 | 57 | void PluginProcessor::releaseResources() 58 | { 59 | // When playback stops, you can use this as an opportunity to free up any 60 | // spare memory, etc. 61 | 62 | DBGLOG("RELEASE called"); 63 | incoming_notes_.clear(); 64 | active_notes_.clear(); 65 | scheduled_notes_.clear(); 66 | 67 | } 68 | 69 | //============================================================================ 70 | void PluginProcessor::update_algorithm(int new_algo) { 71 | 72 | DBGLOG("PluginProcessor::update_algorithm called # ", new_algo) 73 | 74 | if (!algo_obj_ || algo_obj_->get_algo() != new_algo) { 75 | DBGLOG("Changing to new algorithm # ", new_algo); 76 | 77 | // AlgUp and Down will only show up if setStateInformation is 78 | // somehow flawed. But lets be cautions. 79 | switch (new_algo) { 80 | case Algorithm::Random : 81 | algo_obj_ = std::make_unique(¶meters_.random_parameters); 82 | break; 83 | case Algorithm::AlgUp : 84 | parameters_.linear_parameters.direction = LinearParameters::Direction::Up; 85 | parameters_.linear_parameters.zigzag = false; 86 | parameters_.algorithm_index = Algorithm::Linear; 87 | algo_obj_ = std::make_unique(¶meters_.linear_parameters); 88 | break; 89 | case Algorithm::AlgDown : 90 | parameters_.linear_parameters.direction = LinearParameters::Direction::Down; 91 | parameters_.linear_parameters.zigzag = false; 92 | parameters_.algorithm_index = Algorithm::Linear; 93 | algo_obj_ = std::make_unique(¶meters_.linear_parameters); 94 | break; 95 | case Algorithm::Linear : 96 | algo_obj_ = std::make_unique(¶meters_.linear_parameters); 97 | break; 98 | case Algorithm::Spiral : 99 | algo_obj_ = std::make_unique(¶meters_.spiral_parameters); 100 | break; 101 | } 102 | 103 | } 104 | 105 | } 106 | 107 | //============================================================================ 108 | void PluginProcessor::update_position_data(int64 tick_count) { 109 | 110 | double bpm = 120.0; 111 | double quarter_notes_per_beat = 1; 112 | juce::AudioPlayHead::TimeSignature time_sig{4, 4}; 113 | 114 | pd.tick_count = tick_count; 115 | 116 | auto *play_head = getPlayHead(); 117 | 118 | if (play_head) { 119 | auto position = play_head->getPosition(); 120 | if (position) { 121 | pd.is_playing = position->getIsPlaying(); 122 | 123 | auto hostBpm = position->getBpm(); 124 | if (hostBpm) { 125 | bpm = *hostBpm; 126 | } 127 | auto host_time_sig = position->getTimeSignature(); 128 | if (host_time_sig) { 129 | time_sig = *host_time_sig; 130 | } 131 | 132 | quarter_notes_per_beat = 4.0 / time_sig.denominator; 133 | pd.speed = getSpeedFactor(bpm, time_sig); 134 | 135 | auto opt_pos_qn = position->getPpqPosition(); 136 | if (opt_pos_qn) { 137 | // position in slots 138 | pd.set_position(*opt_pos_qn); 139 | } 140 | } 141 | } 142 | 143 | pd.samples_per_qn = static_cast(std::ceil((sample_rate_ * 60.0) / 144 | (bpm * quarter_notes_per_beat))); 145 | 146 | if (! pd.is_playing ) { 147 | // need to synthesize the data 148 | pd.speed = getSpeedFactor(bpm, time_sig); 149 | pd.set_position(fake_clock_sample_count_ / pd.samples_per_qn); 150 | } 151 | 152 | pd.ticks_per_slot = int64(juce::Time::getHighResolutionTicksPerSecond() / 153 | ((bpm * quarter_notes_per_beat) / (pd.speed * 60.0) )); 154 | 155 | } 156 | 157 | //============================================================================ 158 | std::optional PluginProcessor::maybe_play_note( 159 | double for_slot, double start_pos) { 160 | 161 | DBGLOG("maybe_play_note called ", for_slot, " / ", start_pos) 162 | if (incoming_notes_.size() == 0) { 163 | algo_obj_->reset(); 164 | return std::nullopt; 165 | } 166 | 167 | int note_prob = parameters_.probability->get(); 168 | 169 | HashRandom prob_rng{"Probability", parameters_.get_random_seed(), for_slot}; 170 | if (prob_rng.nextInt(0, 101) > note_prob ) { 171 | return std::nullopt; 172 | } 173 | 174 | std::optional retval; 175 | 176 | int new_note = algo_obj_->getNextNote(for_slot, incoming_notes_, notes_changed_); 177 | notes_changed_ = false; 178 | 179 | if (new_note >= 0 ) { 180 | 181 | int range = parameters_.velo_range->get(); 182 | int note_velocity = parameters_.velocity->get(); 183 | 184 | if (range > 0) { 185 | HashRandom vel_rng{"Velocity", parameters_.get_random_seed(), for_slot}; 186 | 187 | 188 | int max = juce::jmin(128, note_velocity + range); 189 | int min = juce::jmax(1, note_velocity - range); 190 | 191 | note_velocity = vel_rng.nextInt(min, max); 192 | } 193 | 194 | retval = juce::MidiMessage::noteOn( 195 | 1, new_note, (std::uint8_t) note_velocity 196 | ); 197 | 198 | double gate = getGate(for_slot); 199 | DBGLOG("--- gate = ", gate) 200 | auto end_tick = int64 (pd.tick_count + 201 | ((start_pos - pd.position_as_slots + gate) * pd.ticks_per_slot )); 202 | 203 | active_notes_.add({new_note, end_tick}); 204 | DBGLOG("--- start ", new_note, " @ ", start_pos, "; end = ", end_tick) 205 | 206 | } else { 207 | DBGLOG("--- algo could not find note to start"); 208 | } 209 | 210 | return retval; 211 | } 212 | 213 | //============================================================================ 214 | void PluginProcessor::schedule_note(double current_pos, double slot_number, 215 | bool can_advance) { 216 | 217 | HashRandom rng{"Humanize", parameters_.get_random_seed(), slot_number}; 218 | 219 | auto advance = parameters_.timing_advance->get() * can_advance; 220 | float variance = rng.nextFloat( 221 | advance, 222 | parameters_.timing_delay->get()); 223 | 224 | double sched_start = slot_number + (variance/100.0); 225 | 226 | if (sched_start >= current_pos ) { 227 | last_scheduled_slot_number_ = slot_number; 228 | scheduled_notes_.addUsingDefaultSort({slot_number, sched_start}); 229 | DBGLOG("--- scheduling slot ", slot_number, " @ ", sched_start); 230 | } else if (can_advance) { 231 | // try again without advance 232 | schedule_note(current_pos, slot_number, false); 233 | } else { 234 | DBGLOG("--- tried to schedule slot ", slot_number, 235 | " @ ", sched_start, " but late (", current_pos, ")"); 236 | } 237 | 238 | } 239 | 240 | //============================================================================ 241 | void PluginProcessor::reset_data(bool clear_incoming) { 242 | DBGLOG(" reset_data called"); 243 | 244 | last_scheduled_slot_number_ = -1.0; 245 | scheduled_notes_.clearQuick(); 246 | //active_notes_.clearQuick(); 247 | 248 | if (clear_incoming) { 249 | incoming_notes_.clearQuick(); 250 | notes_changed_ = false; 251 | } 252 | 253 | DBGLOG(" reset_data finished"); 254 | } 255 | 256 | //============================================================================ 257 | void PluginProcessor::processBlock (juce::AudioBuffer& buffer, 258 | juce::MidiBuffer& midiBuffer) { 259 | 260 | auto ticks = juce::Time::getHighResolutionTicks(); 261 | 262 | //DBGLOG("--- ProcessBlock START") 263 | 264 | auto sample_count = buffer.getNumSamples(); 265 | 266 | for (auto i = 0; i < buffer.getNumChannels(); ++i) 267 | buffer.clear (i, 0, sample_count); 268 | 269 | processMidi(sample_count, ticks, midiBuffer); 270 | 271 | } 272 | 273 | //============================================================================ 274 | void PluginProcessor::processBlock (juce::AudioBuffer& buffer, 275 | juce::MidiBuffer& midiBuffer) { 276 | 277 | 278 | //DBGLOG("--- ProcessBlock START") 279 | 280 | auto ticks = juce::Time::getHighResolutionTicks(); 281 | 282 | auto sample_count = buffer.getNumSamples(); 283 | 284 | for (auto i = 0; i < buffer.getNumChannels(); ++i) 285 | buffer.clear (i, 0, sample_count); 286 | 287 | processMidi(sample_count, ticks, midiBuffer); 288 | 289 | } 290 | 291 | //============================================================================ 292 | void PluginProcessor::processMidi(int sample_count, int64 tick_count, 293 | juce::MidiBuffer& midiBuffer ) { 294 | 295 | if (algo_changed_) { 296 | algo_changed_ = false; 297 | update_algorithm(parameters_.get_algo_index()); 298 | } 299 | 300 | update_position_data(tick_count); 301 | 302 | double slots_in_buffer = (double(sample_count) / double(pd.samples_per_qn)) / pd.speed; 303 | // How long is the buffer in HiRez ticks 304 | // All the casting is due to the fact that slots_in_buffer will 305 | // always be (0, 1] so integer math will come out as zero. 306 | int64 ticks_in_buffer = int64(double(pd.ticks_per_slot) * slots_in_buffer); 307 | 308 | // how many samples in a slot 309 | auto slot_duration = static_cast(std::ceil(double(pd.samples_per_qn) * pd.speed)); 310 | 311 | bool do_cleanup = false; 312 | bool clear_incoming = true; 313 | bool play_transition = false; 314 | 315 | if (pd.is_playing != last_play_state_) { 316 | DBGLOG("--- Play state changed to ", pd.is_playing); 317 | last_play_state_ = pd.is_playing; 318 | do_cleanup = true; 319 | play_transition = true; 320 | } 321 | 322 | if (pd.position_as_slots < last_position_) { 323 | // we must have looped without stopping. 324 | // clear everything and restart. 325 | // This isn't really right. If we are looping without stopping, 326 | // then we should let things that started at the end of the loop 327 | // continue. See also below where we stop all active notes. 328 | DBGLOG("--- LOOPING - resetting data"); 329 | do_cleanup = true; 330 | // During loops, Cubase has a habit of sending the Note-Ons 331 | // that happen at the beginning of the loop in the ending 332 | // block of the loop. Don't clear the notes that we have 333 | // seen so that we hold on to them. 334 | if (host_type.isCubase() && !play_transition) { 335 | DBGLOG("--- LOOPING - Doing Cubase specifics."); 336 | clear_incoming = false; 337 | } 338 | } 339 | last_position_ = pd.position_as_slots; 340 | 341 | if ( do_cleanup) { 342 | reset_data(clear_incoming); 343 | } 344 | 345 | //DBGLOG("processBlock setup done") 346 | 347 | 348 | #if SF_DEBUG 349 | if (pd.is_playing || !midiBuffer.isEmpty() 350 | || !incoming_notes_.isEmpty() 351 | || !active_notes_.isEmpty() ) { 352 | std::stringstream x; 353 | x << "START playing = " << pd.is_playing << "; ppq_pos = " << pd.qn_position 354 | << "; pos_as_slots = " << pd.position_as_slots 355 | << ";\n slot_number = " << pd.slot_number 356 | << "; slot_fraction = " << std::setprecision(7) << pd.slot_fraction 357 | << ";\n ticks_in_buffer = " << ticks_in_buffer 358 | << "; last_sched_slot = " << last_scheduled_slot_number_; 359 | dbgout->logMessage(x.str()); 360 | } 361 | #endif 362 | 363 | 364 | // === Read Midi Messages and update incoming_notes_ set 365 | juce::MidiBuffer newBuffer; 366 | 367 | // These are used if debug is set 368 | int on_count = 0; 369 | int off_count = 0; 370 | int stop_count = 0; 371 | 372 | juce::ignoreUnused(on_count, off_count, stop_count); 373 | 374 | for (const auto metadata : midiBuffer) 375 | { 376 | const auto msg = metadata.getMessage(); 377 | if (msg.isNoteOn()) { 378 | incoming_notes_.add(msg.getNoteNumber()); 379 | notes_changed_ = true; 380 | on_count += 1; 381 | } else if (msg.isNoteOff()) { 382 | if (incoming_notes_.contains(msg.getNoteNumber())) { 383 | incoming_notes_.removeValue(msg.getNoteNumber()); 384 | notes_changed_ = true; 385 | off_count += 1; 386 | } else { 387 | // if we didn't see the NoteOn assume we were bypassed 388 | // and put the message back in the buffer. 389 | newBuffer.addEvent(msg, metadata.samplePosition); 390 | } 391 | } else { 392 | newBuffer.addEvent(msg, metadata.samplePosition); 393 | } 394 | } 395 | 396 | midiBuffer.swapWith(newBuffer); 397 | 398 | juce::Array new_notes{}; 399 | 400 | //DBGLOG("input buffer processed") 401 | 402 | //------------------------------------------------------------------------ 403 | // Stop any notes that will end during this buffer 404 | for (int idx = 0; idx < active_notes_.size(); ++idx) { 405 | 406 | auto thisNote = active_notes_.getUnchecked(idx); 407 | auto note_value = thisNote.note_value; 408 | auto end_tick = thisNote.end_tick; 409 | auto end_buffer = pd.tick_count + ticks_in_buffer; 410 | 411 | if (play_transition && !pd.is_playing) { 412 | // We stopped, so turn all active notes off 413 | DBGLOG("stop ", note_value, " @ ", 2); 414 | midiBuffer.addEvent( 415 | juce::MidiMessage::noteOff (1, note_value), sample_count - 2); 416 | stop_count += 1; 417 | } else if ( end_buffer > end_tick ) { 418 | // This is the number of samples into the buffer where the note should turn off. 419 | int offset = int((end_tick - end_buffer) / pd.ticks_per_slot); 420 | DBGLOG("stop ", note_value, " @ ", offset); 421 | midiBuffer.addEvent(juce::MidiMessage::noteOff (1, note_value), offset); 422 | stop_count += 1; 423 | 424 | } else { 425 | // we didn't stop it, so copy to the new list 426 | new_notes.add(thisNote); 427 | } 428 | } 429 | 430 | active_notes_.clearQuick(); 431 | active_notes_.addArray(new_notes); 432 | 433 | //DBGLOG("active notes processed") 434 | 435 | if (pd.is_playing || !incoming_notes_.isEmpty() || !active_notes_.isEmpty() ) { 436 | DBGLOG("incoming = ", incoming_notes_.size(), "; active = ", 437 | active_notes_.size(), "; changed = ", notes_changed_, 438 | "; on = ", on_count, "; off = ", off_count, 439 | "; stop = ", stop_count 440 | ) 441 | } 442 | 443 | //------------------------------------------------------------------------ 444 | // Schedule note if required. 445 | if (! incoming_notes_.isEmpty()) { 446 | if (last_scheduled_slot_number_ < pd.slot_number) { 447 | schedule_note(pd.position_as_slots, pd.slot_number, false); 448 | } 449 | if ( (pd.slot_fraction > 0.5) && last_scheduled_slot_number_ <= pd.slot_number) { 450 | schedule_note(pd.position_as_slots, pd.slot_number+ 1.0, true); 451 | } 452 | 453 | } 454 | 455 | if (scheduled_notes_.size() > 0) { 456 | DBGLOG("-- sched size = ", scheduled_notes_.size(), " next_sched = " , 457 | scheduled_notes_[0].slot_number , "; next_start = ", 458 | scheduled_notes_[0].start); 459 | } 460 | 461 | while(scheduled_notes_.size() > 0) { 462 | 463 | DBGLOG("-- Spinning on scheduled_notes array") 464 | if ((pd.position_as_slots + slots_in_buffer) > scheduled_notes_[0].start ) { 465 | // start a new note 466 | // This is the number of samples into the buffer where the note should turn on 467 | int offset = juce::jmax(0, int((scheduled_notes_[0].start - pd.position_as_slots) * double(slot_duration))); 468 | 469 | // if the note happens right at the end of the buffer, weird things happen. 470 | // So, don't play it now, wait for the next buffer. 471 | if (offset < sample_count - 2) { 472 | auto msg = maybe_play_note(scheduled_notes_[0].slot_number, scheduled_notes_[0].start ); 473 | if (msg) { 474 | DBGLOG("--- Adding msg to buffer at offset ", offset); 475 | midiBuffer.addEvent(*msg, offset); 476 | } 477 | scheduled_notes_.remove(0); 478 | } else { 479 | break; 480 | } 481 | } else { 482 | break; 483 | } 484 | } 485 | 486 | //DBGLOG("scheduled notes processed") 487 | 488 | 489 | if (pd.is_playing) { 490 | DBGLOG("END ", "incoming = ", incoming_notes_.size(), " active count = ", active_notes_.size()); 491 | } else if (incoming_notes_.isEmpty() && active_notes_.isEmpty()) { 492 | // if the incoming midi has no notes that are still 493 | // playing AND the play head is not moving, then take 494 | // this opportunity to reset the fake clock. 495 | fake_clock_sample_count_ = 0; 496 | } else { 497 | fake_clock_sample_count_ += sample_count; 498 | } 499 | 500 | last_block_call_ = juce::Time::currentTimeMillis(); 501 | 502 | //DBGLOG("-- Done") 503 | 504 | 505 | } 506 | 507 | 508 | //============================================================================ 509 | // returns the "speed" of notes based on the value of a quarter note = 1 510 | double PluginProcessor:: getSpeedFactor(double bpm, const juce::AudioPlayHead::TimeSignature &time_sig) { 511 | auto speed_type = parameters_.speed_type->getIndex(); 512 | double speed_factor = 1; 513 | 514 | double qn_per_beat = (4.0 / time_sig.denominator); 515 | 516 | switch (speed_type) { 517 | case SpeedType::Note : 518 | // The array already has the speed relative to a quarter note. 519 | speed_factor = speed_parameter_values[parameters_.speed->getIndex()].multiplier; 520 | break; 521 | case SpeedType::Bar : 522 | { 523 | // 3/4 = 3 * (4/4) = 3 524 | // 12/8 = 12 * (4/8) = 6 525 | // 2/2 = 2 * (4/2) = 4 526 | auto qn_per_bar = time_sig.numerator * qn_per_beat; 527 | speed_factor = qn_per_bar / parameters_.speed_bar->get(); 528 | } 529 | break; 530 | case SpeedType::MSec : 531 | { 532 | auto ms_per_qn = 1000.0 / ( (bpm / 60.0) * qn_per_beat ); 533 | speed_factor = parameters_.speed_ms->get() / ms_per_qn; 534 | } 535 | break; 536 | default : 537 | jassertfalse; 538 | } 539 | 540 | return speed_factor; 541 | } 542 | 543 | double PluginProcessor::getGate(double slot) { 544 | 545 | DBGLOG("--- getGate called = ", slot) 546 | HashRandom rng{"Gate", parameters_.get_random_seed(), slot}; 547 | auto base = parameters_.gate->get() / 100.0f; 548 | auto range = parameters_.gate_range->get() / 100.0f; 549 | 550 | DBGLOG("--- base = ", base, "; range = ", range) 551 | 552 | if (range > 0 ) { 553 | auto gate = rng.nextFloat(base-range, base+range); 554 | DBGLOG("--- returning ", gate) 555 | return gate; 556 | } else { 557 | DBGLOG("--- returning ", base) 558 | return base; 559 | } 560 | 561 | } 562 | 563 | --------------------------------------------------------------------------------