├── .clang-format ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── modules ├── CMakeLists.txt └── cmake │ ├── SubprojectVersion.cmake │ ├── WarningFlags.cmake │ └── pluginval.cmake ├── preview.png ├── res ├── CMakeLists.txt ├── RobotoCondensed-Bold.ttf ├── RobotoCondensed-Regular.ttf ├── gui.xml ├── knob.svg ├── pointer.svg └── presets │ └── Default.mypreset └── src ├── CMakeLists.txt ├── PluginProcessor.cpp ├── PluginProcessor.hpp ├── dsp ├── BiquadFilter.cpp ├── BiquadFilter.hpp ├── BitReduction.cpp ├── BitReduction.hpp ├── CMakeLists.txt └── WaveWarp.hpp ├── gui ├── CMakeLists.txt ├── item │ └── DynText.hpp └── lnf │ ├── MainLNF.cpp │ └── MainLNF.hpp └── pch.h /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AccessModifierOffset: -4 3 | AlignAfterOpenBracket: Align 4 | AlignConsecutiveAssignments: false 5 | AlignConsecutiveDeclarations: false 6 | AlignEscapedNewlines: Left 7 | AlignOperands: Align 8 | AlignTrailingComments: false 9 | AllowAllParametersOfDeclarationOnNextLine: false 10 | AllowShortBlocksOnASingleLine: Never 11 | AllowShortCaseLabelsOnASingleLine: false 12 | AllowShortFunctionsOnASingleLine: All 13 | AllowShortIfStatementsOnASingleLine: Never 14 | AllowShortLoopsOnASingleLine: false 15 | AlwaysBreakAfterDefinitionReturnType: None 16 | AlwaysBreakAfterReturnType: None 17 | AlwaysBreakBeforeMultilineStrings: false 18 | AlwaysBreakTemplateDeclarations: Yes 19 | BinPackArguments: false 20 | BinPackParameters: false 21 | BreakAfterJavaFieldAnnotations: false 22 | BreakBeforeBinaryOperators: NonAssignment 23 | BreakBeforeBraces: Allman 24 | BreakBeforeTernaryOperators: true 25 | BreakConstructorInitializersBeforeComma: false 26 | BreakStringLiterals: false 27 | ColumnLimit: 0 28 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 29 | ConstructorInitializerIndentWidth: 4 30 | ContinuationIndentWidth: 4 31 | Cpp11BracedListStyle: false 32 | DerivePointerAlignment: false 33 | DisableFormat: false 34 | ExperimentalAutoDetectBinPacking: false 35 | ForEachMacros: ['forEachXmlChildElement'] 36 | IndentCaseLabels: true 37 | IndentWidth: 4 38 | IndentWrappedFunctionNames: true 39 | KeepEmptyLinesAtTheStartOfBlocks: false 40 | Language: Cpp 41 | MaxEmptyLinesToKeep: 1 42 | NamespaceIndentation: Inner 43 | PointerAlignment: Left 44 | ReflowComments: false 45 | SortIncludes: true 46 | SpaceAfterCStyleCast: true 47 | SpaceAfterLogicalNot: true 48 | SpaceBeforeAssignmentOperators: true 49 | SpaceBeforeCpp11BracedList: true 50 | SpaceBeforeParens: NonEmptyParentheses 51 | SpaceInEmptyParentheses: false 52 | SpaceBeforeInheritanceColon: true 53 | SpacesInAngles: false 54 | SpacesInCStyleCastParentheses: false 55 | SpacesInContainerLiterals: true 56 | SpacesInParentheses: false 57 | SpacesInSquareBrackets: false 58 | Standard: "c++17" 59 | TabWidth: 4 60 | UseTab: Never 61 | --- 62 | Language: ObjC 63 | BasedOnStyle: Chromium 64 | AlignTrailingComments: true 65 | BreakBeforeBraces: Allman 66 | ColumnLimit: 0 67 | IndentWidth: 4 68 | KeepEmptyLinesAtTheStartOfBlocks: false 69 | ObjCSpaceAfterProperty: true 70 | ObjCSpaceBeforeProtocolList: true 71 | PointerAlignment: Left 72 | SpacesBeforeTrailingComments: 1 73 | TabWidth: 4 74 | UseTab: Never 75 | ... 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artifacts 2 | build*/ 3 | bin/ 4 | *_Packaged 5 | *.exe 6 | *.dmg 7 | 8 | # IDE settings 9 | .vscode/ 10 | .idea/ 11 | 12 | # Mac extras 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "modules/JUCE"] 2 | path = modules/JUCE 3 | url = git@github.com:juce-framework/JUCE.git 4 | ignore = dirty 5 | [submodule "modules/foleys_gui_magic"] 6 | path = modules/foleys_gui_magic 7 | url = git@github.com:ffAudio/foleys_gui_magic.git 8 | ignore = dirty 9 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | set(CMAKE_OSX_DEPLOYMENT_TARGET "10.12" CACHE STRING "Minimum OS X deployment target") 3 | project(HandyOsc VERSION 1.0.0) 4 | 5 | set(CMAKE_CXX_STANDARD 17) 6 | add_subdirectory(modules) 7 | include_directories(modules) 8 | 9 | # juce_set_vst2_sdk_path(C:/SDKs/VST_SDK/VST2_SDK/) 10 | # juce_set_aax_sdk_path(NONE) 11 | 12 | # set default plugin formats to build 13 | if(IOS) 14 | set(JUCE_FORMATS Standalone AUv3) 15 | else() 16 | set(JUCE_FORMATS AU VST3 Standalone) 17 | endif() 18 | 19 | # Build LV2 only on Linux 20 | if(UNIX AND NOT APPLE) 21 | message(STATUS "Building LV2 plugin format") 22 | list(APPEND JUCE_FORMATS LV2) 23 | endif() 24 | 25 | # Build VST2 if SDK target exists 26 | if(TARGET juce_vst2_sdk) 27 | message(STATUS "Building VST2 plugin format") 28 | list(APPEND JUCE_FORMATS VST) 29 | endif() 30 | 31 | # Build AAX if SDK target exists 32 | if(TARGET juce_aax_sdk) 33 | message(STATUS "Building AAX plugin format") 34 | list(APPEND JUCE_FORMATS AAX) 35 | endif() 36 | 37 | juce_add_plugin(HandyOsc 38 | COMPANY_NAME Pachuch 39 | PLUGIN_MANUFACTURER_CODE PHCH 40 | PLUGIN_CODE spg3 41 | FORMATS ${JUCE_FORMATS} 42 | ProductName "HandyOsc" 43 | 44 | VST2_CATEGORY kPlugCategEffect 45 | VST3_CATEGORIES Fx Delay 46 | AU_MAIN_TYPE kAudioUnitType_Effect 47 | AAX_CATEGORY AAX_ePlugInCategory_Delay 48 | 49 | LV2URI https://github.com/Pachuch/HandyOsc 50 | 51 | # ICON_BIG res/logo.png 52 | MICROPHONE_PERMISSION_ENABLED TRUE 53 | NEEDS_STORE_KIT TRUE 54 | REQUIRES_FULL_SCREEN TRUE 55 | IPHONE_SCREEN_ORIENTATIONS UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight 56 | IPAD_SCREEN_ORIENTATIONS UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight 57 | ) 58 | 59 | # create JUCE header 60 | juce_generate_juce_header(HandyOsc) 61 | 62 | # add sources 63 | add_subdirectory(src) 64 | include_directories(src) 65 | add_subdirectory(res) 66 | 67 | target_compile_definitions(HandyOsc 68 | PUBLIC 69 | JUCE_DISPLAY_SPLASH_SCREEN=0 70 | JUCE_REPORT_APP_USAGE=0 71 | JUCE_WEB_BROWSER=0 72 | JUCE_USE_CURL=0 73 | JUCE_VST3_CAN_REPLACE_VST2=1 74 | EXAMPLE_AUTO_UPDATE=1 75 | ) 76 | 77 | if(ASIOSDK_DIR) 78 | message(STATUS "Using ASIO SDK from ${ASIOSDK_DIR}") 79 | target_include_directories(juce_plugin_modules PUBLIC ${ASIOSDK_DIR}/common) 80 | target_compile_definitions(juce_plugin_modules PUBLIC JUCE_ASIO=1) 81 | endif() 82 | 83 | target_link_libraries(HandyOsc PUBLIC 84 | juce_plugin_modules 85 | ) 86 | 87 | option(RUN_PLUGINVAL "Run pluginval on HandyOsc plugins" OFF) 88 | if(RUN_PLUGINVAL) 89 | create_pluginval_target(HandyOsc_VST3 "HandyOsc.vst3") 90 | endif() 91 | 92 | # we need these flags for notarization on MacOS 93 | option(MACOS_RELEASE "Set build flags for MacOS Release" OFF) 94 | if(MACOS_RELEASE) 95 | message(STATUS "Setting MacOS release flags...") 96 | set_target_properties(HandyOsc_Standalone PROPERTIES 97 | XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES) 98 | endif() 99 | 100 | if(IOS) 101 | message(STATUS "Setting iOS-specific properties...") 102 | 103 | foreach(target IN ITEMS BinaryData polylogarithm juce_plugin_modules HandyOsc HandyOsc_Standalone HandyOsc_AUv3) 104 | set_target_properties(${target} 105 | PROPERTIES 106 | RUNTIME_OUTPUT_DIRECTORY "./" 107 | ARCHIVE_OUTPUT_DIRECTORY "./" 108 | LIBRARY_OUTPUT_DIRECTORY "./") 109 | endforeach() 110 | 111 | set_target_properties(HandyOsc_Standalone PROPERTIES 112 | XCODE_ATTRIBUTE_INSTALL_PATH "$(LOCAL_APPS_DIR)" 113 | XCODE_ATTRIBUTE_SKIP_INSTALL "NO" 114 | XCODE_ATTRIBUTE_ENABLE_IN_APP_PURCHASE "YES") 115 | 116 | set_target_properties(HandyOsc_AUv3 PROPERTIES 117 | XCODE_ATTRIBUTE_INSTALL_PATH "$(LOCAL_APPS_DIR)/HandyOsc.app/PlugIns" 118 | XCODE_ATTRIBUTE_SKIP_INSTALL "NO" 119 | XCODE_ATTRIBUTE_ENABLE_IN_APP_PURCHASE "YES") 120 | endif() 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Pachuch 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HandyOsc Audio Plugin - Turn any audio into a digital synth 2 | 3 |

4 | 5 |

6 | 7 | This is a effect plugin that uses wave warping logic to create sawtooth-like waves from input audio. Combine warp and bit reduction to get new sounds! 8 | * Supported hosts: VST3 only. 9 | 10 | I haven't tested other plugin formats other than VST3. But JUCE supports VST, VST3, AU, AUv3, AAX and LV2 audio plug-ins, I might get lucky. 11 | 12 | # Features: 13 | 1. 3 waveform warping algorithms 14 | 2. Bitcrush 15 | 3. Pre/Post filter 16 | 4. Oscilloscope 17 | 18 | # Dependencies 19 | 20 | Build on top of the JUCE framework and foleys_gui_magic GUI builder. 21 | 22 | # Build 23 | 24 | Examples: 25 | * Windows + MSVC: 26 | ```console 27 | mkdir build 28 | cd build 29 | cmake .. -G "Visual Studio 17 2022" 30 | cmake --build . --target HandyOsc_VST3 --config Release 31 | ``` 32 | 33 | * Linux + GCC: 34 | ```console 35 | mkdir build 36 | cd build 37 | cmake .. -G "Unix Makefiles" 38 | cmake --build . --target HandyOsc_VST3 --config Release 39 | ``` -------------------------------------------------------------------------------- /modules/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(JUCE) 2 | include(cmake/SubprojectVersion.cmake) 3 | subproject_version(JUCE juce_version) 4 | message(STATUS "VERSION for JUCE: ${juce_version}") 5 | 6 | juce_add_modules(foleys_gui_magic/modules/foleys_gui_magic) 7 | 8 | include(cmake/WarningFlags.cmake) 9 | add_library(juce_plugin_modules STATIC) 10 | target_link_libraries(juce_plugin_modules 11 | PRIVATE 12 | BinaryData 13 | juce::juce_audio_utils 14 | juce::juce_audio_plugin_client 15 | juce::juce_dsp 16 | foleys_gui_magic 17 | PUBLIC 18 | juce::juce_recommended_config_flags 19 | juce::juce_recommended_lto_flags 20 | warning_flags 21 | ) 22 | 23 | target_compile_definitions(juce_plugin_modules 24 | PUBLIC 25 | JUCE_DISPLAY_SPLASH_SCREEN=0 26 | JUCE_REPORT_APP_USAGE=0 27 | JUCE_WEB_BROWSER=0 28 | JUCE_USE_CURL=0 29 | JUCE_VST3_CAN_REPLACE_VST2=0 30 | JUCE_JACK=1 31 | JUCE_ALSA=1 32 | JUCE_MODAL_LOOPS_PERMITTED=1 33 | FOLEYS_SHOW_GUI_EDITOR_PALLETTE=0 34 | FOLEYS_ENABLE_BINARY_DATA=1 35 | JucePlugin_Manufacturer="Pachuch" 36 | JucePlugin_VersionString="${CMAKE_PROJECT_VERSION}" 37 | JucePlugin_Name="${CMAKE_PROJECT_NAME}" 38 | INTERFACE 39 | $ 40 | ) 41 | 42 | target_include_directories(juce_plugin_modules 43 | INTERFACE 44 | $ 45 | ) 46 | 47 | set_target_properties(juce_plugin_modules PROPERTIES 48 | POSITION_INDEPENDENT_CODE TRUE 49 | VISIBILITY_INLINES_HIDDEN TRUE 50 | C_VISBILITY_PRESET hidden 51 | CXX_VISIBILITY_PRESET hidden 52 | ) 53 | 54 | if(IOS) 55 | target_link_libraries(juce_plugin_modules PRIVATE juce::juce_product_unlocking) 56 | target_compile_definitions(juce_plugin_modules PUBLIC JUCE_IN_APP_PURCHASES=1) 57 | endif() 58 | 59 | if(NOT APPLE) 60 | message(STATUS "Linking with OpenGL") 61 | target_link_libraries(juce_plugin_modules PRIVATE juce::juce_opengl) 62 | target_compile_definitions(juce_plugin_modules PUBLIC FOLEYS_ENABLE_OPEN_GL_CONTEXT=1) 63 | endif() 64 | 65 | include(cmake/pluginval.cmake) 66 | -------------------------------------------------------------------------------- /modules/cmake/SubprojectVersion.cmake: -------------------------------------------------------------------------------- 1 | # subproject_version( ) 2 | # 3 | # Extract version of a sub-project, which was previously included with add_subdirectory(). 4 | function(subproject_version subproject_name VERSION_VAR) 5 | # Read CMakeLists.txt for subproject and extract project() call(s) from it. 6 | file(STRINGS "${${subproject_name}_SOURCE_DIR}/CMakeLists.txt" project_calls REGEX "[ \t]*project\\(") 7 | # For every project() call try to extract its VERSION option 8 | foreach(project_call ${project_calls}) 9 | string(REGEX MATCH "VERSION[ ]+([^ )]+)" version_param "${project_call}") 10 | if(version_param) 11 | set(version_value "${CMAKE_MATCH_1}") 12 | endif() 13 | endforeach() 14 | if(version_value) 15 | set(${VERSION_VAR} "${version_value}" PARENT_SCOPE) 16 | else() 17 | message("WARNING: Cannot extract version for subproject '${subproject_name}'") 18 | endif() 19 | 20 | endfunction(subproject_version) 21 | -------------------------------------------------------------------------------- /modules/cmake/WarningFlags.cmake: -------------------------------------------------------------------------------- 1 | add_library(warning_flags INTERFACE) 2 | 3 | if((CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") OR (CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC")) 4 | target_compile_options(warning_flags INTERFACE 5 | /W4 # base warning level 6 | /wd4458 # declaration hides class member (from Foley's GUI Magic) 7 | /wd4505 # since VS2019 doesn't handle [[ maybe_unused ]] for static functions (RTNeural::debug_print) 8 | ) 9 | elseif((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") OR (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")) 10 | target_compile_options(warning_flags INTERFACE 11 | -Wall -Wshadow-all -Wshorten-64-to-32 -Wstrict-aliasing -Wuninitialized 12 | -Wunused-parameter -Wconversion -Wsign-compare -Wint-conversion 13 | -Wconditional-uninitialized -Woverloaded-virtual -Wreorder 14 | -Wconstant-conversion -Wsign-conversion -Wunused-private-field 15 | -Wbool-conversion -Wno-extra-semi -Wunreachable-code 16 | -Wzero-as-null-pointer-constant -Wcast-align 17 | -Wno-inconsistent-missing-destructor-override -Wshift-sign-overflow 18 | -Wnullable-to-nonnull-conversion -Wno-missing-field-initializers 19 | -Wno-ignored-qualifiers -Wpedantic -Wno-pessimizing-move 20 | # These lines suppress some custom warnings. 21 | # Comment them out to be more strict. 22 | -Wno-shadow-field-in-constructor 23 | # Needed for ARM processor, OSX versions below 10.14 24 | -fno-aligned-allocation 25 | ) 26 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 27 | target_compile_options(warning_flags INTERFACE 28 | -Wall -Wextra -Wstrict-aliasing -Wuninitialized -Wunused-parameter 29 | -Wsign-compare -Woverloaded-virtual -Wreorder -Wunreachable-code 30 | -Wzero-as-null-pointer-constant -Wcast-align -Wno-implicit-fallthrough 31 | -Wno-maybe-uninitialized -Wno-missing-field-initializers -Wno-pedantic 32 | -Wno-ignored-qualifiers -Wno-unused-function -Wno-pessimizing-move 33 | # From LV2 Wrapper 34 | -Wno-parentheses -Wno-deprecated-declarations -Wno-redundant-decls 35 | # These lines suppress some custom warnings. 36 | # Comment them out to be more strict. 37 | -Wno-redundant-move 38 | ) 39 | 40 | if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER "7.0.0") 41 | target_compile_options(warning_flags INTERFACE "-Wno-strict-overflow") 42 | endif() 43 | endif() 44 | -------------------------------------------------------------------------------- /modules/cmake/pluginval.cmake: -------------------------------------------------------------------------------- 1 | set_property(GLOBAL PROPERTY pluginval_setup) 2 | function(create_pluginval_target target plugin) 3 | message(STATUS "Creating pluginval target for ${target}") 4 | 5 | if (APPLE) 6 | set(pluginval_url "https://github.com/Tracktion/pluginval/releases/latest/download/pluginval_macOS.zip") 7 | set(pluginval_exe pluginval/pluginval.app/Contents/MacOS/pluginval) 8 | elseif (WIN32) 9 | set(pluginval_url "https://github.com/Tracktion/pluginval/releases/latest/download/pluginval_Windows.zip") 10 | set(pluginval_exe pluginval/pluginval.exe) 11 | else () 12 | set(pluginval_url "https://github.com/Tracktion/pluginval/releases/latest/download/pluginval_Linux.zip") 13 | set(pluginval_exe pluginval/pluginval) 14 | endif () 15 | 16 | get_property(is_pluginval_setup GLOBAL PROPERTY pluginval_setup) 17 | if ("${is_pluginval_setup}" STREQUAL "yes") 18 | message(STATUS "Skipping pluginval setup...") 19 | else () 20 | add_custom_target(download-pluginval) 21 | add_custom_command(TARGET download-pluginval 22 | POST_BUILD 23 | WORKING_DIRECTORY ${CMAKE_BINARY_DIR} 24 | COMMAND cmake -E make_directory pluginval 25 | COMMAND curl -L ${pluginval_url} -o pluginval/pluginval.zip 26 | COMMAND cd pluginval && unzip -o pluginval.zip 27 | ) 28 | add_custom_target(pluginval-all) 29 | set_property(GLOBAL PROPERTY pluginval_setup "yes") 30 | endif() 31 | 32 | set(name "${target}-pluginval") 33 | add_custom_target(${name}) 34 | add_dependencies(${name} ${target}) 35 | add_dependencies(${name} download-pluginval) 36 | get_target_property(plugin_location ${target} LIBRARY_OUTPUT_DIRECTORY) 37 | add_custom_command(TARGET ${name} 38 | POST_BUILD 39 | WORKING_DIRECTORY ${CMAKE_BINARY_DIR} 40 | COMMAND ${pluginval_exe} --validate-in-process --strictness-level 5 --timeout-ms 600000 --output-dir "." --validate "${plugin_location}/${plugin}" || exit 1 41 | ) 42 | add_dependencies(pluginval-all ${name}) 43 | endfunction() 44 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulmour/HandyOsc/0b00d59d2a83eefe13d7d1c39a40ed4a74b6e3b2/preview.png -------------------------------------------------------------------------------- /res/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | juce_add_binary_data(BinaryData SOURCES 2 | gui.xml 3 | 4 | knob.svg 5 | pointer.svg 6 | RobotoCondensed-Bold.ttf 7 | RobotoCondensed-Regular.ttf 8 | 9 | presets/Default.mypreset 10 | ) 11 | 12 | # Need to build BinaryData with -fPIC flag on Linux 13 | set_target_properties(BinaryData PROPERTIES 14 | POSITION_INDEPENDENT_CODE TRUE) 15 | -------------------------------------------------------------------------------- /res/RobotoCondensed-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulmour/HandyOsc/0b00d59d2a83eefe13d7d1c39a40ed4a74b6e3b2/res/RobotoCondensed-Bold.ttf -------------------------------------------------------------------------------- /res/RobotoCondensed-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eulmour/HandyOsc/0b00d59d2a83eefe13d7d1c39a40ed4a74b6e3b2/res/RobotoCondensed-Regular.ttf -------------------------------------------------------------------------------- /res/gui.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | 24 | 25 | 27 | 28 | 30 | 32 | 34 | 36 | 38 | 39 | 40 | 42 | 44 | 45 | 46 | 47 | 49 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 64 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /res/knob.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /res/pointer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /res/presets/Default.mypreset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | option(BUILD_HEADLESS "Build HandyOsc headless utilities" OFF) 2 | if(NOT (IOS OR MACOS_RELEASE) AND BUILD_HEADLESS) 3 | message(STATUS "Building HandyOsc Headless!") 4 | add_subdirectory(headless) 5 | endif() 6 | 7 | file (GLOB FILES_SRC CONFIGURE_DEPENDS "*.hpp" "*.cpp") 8 | 9 | # target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE "gui") 10 | 11 | target_sources(HandyOsc PRIVATE ${FILES_SRC}) 12 | 13 | target_precompile_headers(HandyOsc PRIVATE pch.h) 14 | 15 | add_subdirectory("dsp") 16 | add_subdirectory("gui") 17 | -------------------------------------------------------------------------------- /src/PluginProcessor.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | This file contains the basic framework code for a JUCE plugin processor. 5 | 6 | ============================================================================== 7 | */ 8 | 9 | #include "PluginProcessor.hpp" 10 | 11 | namespace IDs { 12 | 13 | static juce::String ioGain { "ioGain" }; 14 | static juce::String ioPeak { "ioPeak" }; 15 | static juce::String ioReductionMix { "ioReductionMix" }; 16 | static juce::String ioWarpMix { "ioWarpMix" }; 17 | 18 | static juce::String controlReduction { "controlReduction" }; 19 | static juce::String controlWarp { "controlWarp" }; 20 | static juce::String controlLogicType { "controlLogicType" }; 21 | static juce::String filterCutoff{ "filterCutoff" }; 22 | static juce::String filterResonance{ "filterResonance" }; 23 | static juce::String filterPlace{ "filterPlace" }; 24 | static juce::Identifier oscilloscope { "oscilloscope" }; 25 | 26 | } // namespace IDs 27 | 28 | juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout() 29 | { 30 | juce::AudioProcessorValueTreeState::ParameterLayout layout; 31 | 32 | auto controlGroup = std::make_unique ("controlGroup", TRANS ("Control"), "|"); 33 | controlGroup->addChild ( 34 | std::make_unique (IDs::controlLogicType, "Warp Type", juce::StringArray("A", "B", "C"), 0), 35 | std::make_unique (IDs::controlReduction, "Reduction", 1, 16, 16), 36 | std::make_unique (IDs::controlWarp, "Warp", juce::NormalisableRange (0.f, 1.f, 0.f, 0.25f), 0.005f) 37 | ); 38 | 39 | auto filterGroup = std::make_unique ("filterGroup", TRANS ("Filter"), "|"); 40 | filterGroup->addChild ( 41 | std::make_unique (IDs::filterPlace, "Placement", juce::StringArray("Off", "Pre", "Post"), 1), 42 | std::make_unique (IDs::filterCutoff, "Cutoff", juce::NormalisableRange (20.f, 20000.f, 0.f, .25f), 20000.f), 43 | std::make_unique (IDs::filterResonance, "Resonance", juce::NormalisableRange (0.1f, 96.f, 0.f, .25f), .1f) 44 | ); 45 | 46 | auto ioGroup = std::make_unique ("ioGroup", TRANS ("I/O"), "|"); 47 | ioGroup->addChild ( 48 | std::make_unique (IDs::ioGain, "Gain", juce::NormalisableRange (0.f, 2.f), 1.0f), 49 | std::make_unique (IDs::ioReductionMix, "Reduction Mix", juce::NormalisableRange (0.f, 1.f), 1.0f), 50 | std::make_unique (IDs::ioWarpMix, "Warp Mix", juce::NormalisableRange (0.f, 1.f), 1.0f) 51 | ); 52 | 53 | layout.add (std::move (controlGroup), std::move(filterGroup), std::move (ioGroup)); 54 | 55 | return layout; 56 | } 57 | 58 | //============================================================================== 59 | ExampleAudioProcessor::ExampleAudioProcessor() 60 | #ifndef JucePlugin_PreferredChannelConfigurations 61 | : AudioProcessor (BusesProperties() 62 | #if ! JucePlugin_IsMidiEffect 63 | #if ! JucePlugin_IsSynth 64 | .withInput ("Input", juce::AudioChannelSet::stereo(), true) 65 | #endif 66 | .withOutput ("Output", juce::AudioChannelSet::stereo(), true) 67 | #endif 68 | ) 69 | , vts (*this, nullptr, juce::Identifier ("Parameters"), createParameterLayout()), 70 | biquadFilter(3000.f, 1.f) 71 | #endif 72 | { 73 | this->controlReduction = vts.getRawParameterValue (IDs::controlReduction); 74 | jassert (this->controlReduction != nullptr); 75 | 76 | this->controlWarp = vts.getRawParameterValue (IDs::controlWarp); 77 | jassert (this->controlWarp != nullptr); 78 | 79 | this->filterCutoff = vts.getRawParameterValue (IDs::filterCutoff); 80 | jassert (this->filterCutoff != nullptr); 81 | 82 | this->filterResonance = vts.getRawParameterValue (IDs::filterResonance); 83 | jassert (this->filterResonance != nullptr); 84 | 85 | this->ioGain = vts.getRawParameterValue (IDs::ioGain); 86 | jassert (this->ioGain != nullptr); 87 | 88 | this->ioReductionMix = vts.getRawParameterValue (IDs::ioReductionMix); 89 | jassert (this->ioReductionMix != nullptr); 90 | 91 | this->ioWarpMix = vts.getRawParameterValue (IDs::ioWarpMix); 92 | jassert (this->ioWarpMix != nullptr); 93 | 94 | this->waveWarp.type = vts.getRawParameterValue (IDs::controlLogicType); 95 | jassert (this->waveWarp.type != nullptr); 96 | 97 | vts.addParameterListener (IDs::controlReduction, this); 98 | vts.addParameterListener (IDs::controlWarp, this); 99 | vts.addParameterListener (IDs::filterCutoff, this); 100 | vts.addParameterListener (IDs::filterResonance, this); 101 | vts.addParameterListener (IDs::filterPlace, this); 102 | vts.addParameterListener (IDs::ioGain, this); 103 | vts.addParameterListener (IDs::ioReductionMix, this); 104 | vts.addParameterListener (IDs::ioWarpMix, this); 105 | 106 | oscilloscope = magicState.createAndAddObject (IDs::oscilloscope, 0); 107 | // peakInfo = magicState.createAndAddObject (IDs::oscilloscope, 0); 108 | magicState.setGuiValueTree (BinaryData::gui_xml, BinaryData::gui_xmlSize); 109 | } 110 | 111 | ExampleAudioProcessor::~ExampleAudioProcessor() = default; 112 | 113 | //============================================================================== 114 | const juce::String ExampleAudioProcessor::getName() const 115 | { 116 | return JucePlugin_Name; 117 | } 118 | 119 | bool ExampleAudioProcessor::acceptsMidi() const 120 | { 121 | #if JucePlugin_WantsMidiInput 122 | return true; 123 | #else 124 | return false; 125 | #endif 126 | } 127 | 128 | bool ExampleAudioProcessor::producesMidi() const 129 | { 130 | #if JucePlugin_ProducesMidiOutput 131 | return true; 132 | #else 133 | return false; 134 | #endif 135 | } 136 | 137 | bool ExampleAudioProcessor::isMidiEffect() const 138 | { 139 | #if JucePlugin_IsMidiEffect 140 | return true; 141 | #else 142 | return false; 143 | #endif 144 | } 145 | 146 | double ExampleAudioProcessor::getTailLengthSeconds() const 147 | { 148 | return 0.0; 149 | } 150 | 151 | int ExampleAudioProcessor::getNumPrograms() 152 | { 153 | return 1; // NB: some hosts don't cope very well if you tell them there are 0 programs, 154 | // so this should be at least 1, even if you're not really implementing programs. 155 | } 156 | 157 | int ExampleAudioProcessor::getCurrentProgram() 158 | { 159 | return 0; 160 | } 161 | 162 | void ExampleAudioProcessor::setCurrentProgram (int index) 163 | { 164 | (void)index; 165 | } 166 | 167 | const juce::String ExampleAudioProcessor::getProgramName (int index) 168 | { 169 | (void)index; 170 | return {}; 171 | } 172 | 173 | void ExampleAudioProcessor::changeProgramName (int index, const juce::String& newName) 174 | { 175 | (void)index; 176 | (void)newName; 177 | } 178 | 179 | //============================================================================== 180 | void ExampleAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) 181 | { 182 | // Use this method as the place to do any pre-playback 183 | // initialisation that you need.. 184 | 185 | this->biquadFilter.setSampleRate(sampleRate); 186 | 187 | // MAGIC GUI: this will setup all internals like MagicPlotSources etc. 188 | magicState.prepareToPlay (sampleRate, samplesPerBlock); 189 | } 190 | 191 | void ExampleAudioProcessor::releaseResources() 192 | { 193 | // When playback stops, you can use this as an opportunity to free up any 194 | // spare memory, etc. 195 | } 196 | 197 | #ifndef JucePlugin_PreferredChannelConfigurations 198 | bool ExampleAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const 199 | { 200 | #if JucePlugin_IsMidiEffect 201 | juce::ignoreUnused (layouts); 202 | return true; 203 | #else 204 | // This is the place where you check if the layout is supported. 205 | // In this template code we only support mono or stereo. 206 | // Some plugin hosts, such as certain GarageBand versions, will only 207 | // load plugins that support stereo bus layouts. 208 | if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() 209 | && layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) 210 | return false; 211 | 212 | // This checks if the input layout matches the output layout 213 | #if ! JucePlugin_IsSynth 214 | if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet()) 215 | return false; 216 | #endif 217 | 218 | return true; 219 | #endif 220 | } 221 | #endif 222 | 223 | void ExampleAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) 224 | { 225 | juce::ScopedNoDenormals noDenormals; 226 | auto totalNumInputChannels = getTotalNumInputChannels(); 227 | auto totalNumOutputChannels = getTotalNumOutputChannels(); 228 | 229 | // In case we have more outputs than inputs, this code clears any output 230 | // channels that didn't contain input data, (because these aren't 231 | // guaranteed to be empty - they may contain garbage). 232 | // This is here to avoid people getting screaming feedback 233 | // when they first compile a plugin, but obviously you don't need to keep 234 | // this code if your algorithm always overwrites all the output channels. 235 | for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) 236 | buffer.clear (i, 0, buffer.getNumSamples()); 237 | 238 | // juce::dsp::AudioBlock inBuffer(buffer); 239 | juce::dsp::AudioBlock outBuffer(buffer); 240 | 241 | // This is the place where you'd normally do the guts of your plugin's 242 | // audio processing... 243 | // Make sure to reset the state if your inner loop is processing 244 | // the samples and the outer loop is handling the channels. 245 | // Alternatively, you can process the samples with the channels 246 | // interleaved by keeping the same state. 247 | switch (this->filterPlacement) { 248 | case FilterPlacement::Pre: 249 | this->biquadFilter.process>(outBuffer); 250 | 251 | this->bitReduction.process>( 252 | outBuffer, this->controlReduction->load()); 253 | 254 | this->waveWarp.process>( 255 | outBuffer, this->controlWarp->load()); 256 | break; 257 | case FilterPlacement::Post: 258 | this->bitReduction.process>( 259 | outBuffer, this->controlReduction->load()); 260 | 261 | this->waveWarp.process>( 262 | outBuffer, this->controlWarp->load()); 263 | 264 | this->biquadFilter.process>(outBuffer); 265 | break; 266 | default: 267 | this->bitReduction.process>( 268 | outBuffer, this->controlReduction->load()); 269 | 270 | this->waveWarp.process>( 271 | outBuffer, this->controlWarp->load()); 272 | } 273 | 274 | for (int channel = 0; channel < totalNumInputChannels; ++channel) 275 | { 276 | auto sample = buffer.getWritePointer (channel); 277 | 278 | for (int i = 0; i < buffer.getNumSamples(); ++i) 279 | { 280 | sample[i] *= this->ioGain->load(); 281 | } 282 | } 283 | 284 | // magicState.getGuiTree(). 285 | oscilloscope->pushSamples(buffer); 286 | } 287 | 288 | //============================================================================== 289 | bool ExampleAudioProcessor::hasEditor() const 290 | { 291 | return true; // (change this to false if you choose to not supply an editor) 292 | } 293 | 294 | // juce::SharedResourcePointer; 295 | juce::AudioProcessorEditor* ExampleAudioProcessor::createEditor() 296 | { 297 | this->mainLNF = std::make_shared(); 298 | 299 | // Register GUI items for Foleys GUI Magic 300 | auto builder = std::make_unique (magicState); 301 | 302 | builder->registerJUCEFactories(); 303 | builder->registerJUCELookAndFeels(); 304 | 305 | builder->registerLookAndFeel("MainLNF", std::make_unique()); 306 | LookAndFeel::setDefaultLookAndFeel(this->mainLNF.get()); 307 | 308 | builder->registerFactory("DynText", &DynTextItem::factory); 309 | 310 | auto editor = new foleys::MagicPluginEditor (magicState, std::move (builder)); 311 | 312 | editor->setResizeLimits (260, 500, 520, 800); 313 | editor->setSize (260, 500); 314 | return editor; 315 | } 316 | 317 | //============================================================================== 318 | void ExampleAudioProcessor::getStateInformation (juce::MemoryBlock& destData) 319 | { 320 | (void)destData; 321 | // You should use this method to store your parameters in the memory block. 322 | // You could do that either as raw data, or use the XML or ValueTree classes 323 | // as intermediaries to make it easy to save and load complex data. 324 | } 325 | 326 | void ExampleAudioProcessor::setStateInformation (const void* data, int sizeInBytes) 327 | { 328 | (void)data; 329 | (void)sizeInBytes; 330 | // You should use this method to restore your parameters from this memory block, 331 | // whose contents will have been created by the getStateInformation() call. 332 | } 333 | 334 | void ExampleAudioProcessor::parameterChanged (const juce::String& param, float value) 335 | { 336 | if (param == IDs::filterCutoff) { 337 | biquadFilter.setFrequency(value); 338 | } else if (param == IDs::filterResonance) { 339 | biquadFilter.setResonance (value); 340 | } else if (param == IDs::filterPlace) { 341 | switch (juce::roundToInt(value)) { 342 | case 1: this->filterPlacement = FilterPlacement::Pre; break; 343 | case 2: this->filterPlacement = FilterPlacement::Post; break; 344 | default: this->filterPlacement = FilterPlacement::Off; break; 345 | } 346 | } else if (param == IDs::ioReductionMix) { 347 | this->bitReduction.setMixAmount(value); 348 | } else if (param == IDs::ioWarpMix) { 349 | this->waveWarp.setMixAmount(value); 350 | } 351 | } 352 | 353 | //============================================================================== 354 | // This creates new instances of the plugin.. 355 | juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() 356 | { 357 | return new ExampleAudioProcessor(); 358 | } 359 | -------------------------------------------------------------------------------- /src/PluginProcessor.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | This file contains the basic framework code for a JUCE plugin processor. 5 | 6 | ============================================================================== 7 | */ 8 | 9 | #pragma once 10 | 11 | #include "JuceHeader.h" 12 | #include "foleys_gui_magic/foleys_gui_magic.h" 13 | 14 | #include "gui/lnf/MainLNF.hpp" 15 | #include "gui/item/DynText.hpp" 16 | 17 | #include "dsp/BitReduction.hpp" 18 | #include "dsp/WaveWarp.hpp" 19 | #include "dsp/BiquadFilter.hpp" 20 | 21 | //============================================================================== 22 | /** 23 | */ 24 | class ExampleAudioProcessor : public juce::AudioProcessor, 25 | private juce::AudioProcessorValueTreeState::Listener 26 | #if JucePlugin_Enable_ARA 27 | , public juce::AudioProcessorARAExtension 28 | #endif 29 | { 30 | public: 31 | //============================================================================== 32 | ExampleAudioProcessor(); 33 | ~ExampleAudioProcessor() override; 34 | 35 | //============================================================================== 36 | void prepareToPlay (double sampleRate, int samplesPerBlock) override; 37 | void releaseResources() override; 38 | 39 | #ifndef JucePlugin_PreferredChannelConfigurations 40 | bool isBusesLayoutSupported (const BusesLayout& layouts) const override; 41 | #endif 42 | 43 | void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; 44 | 45 | //============================================================================== 46 | juce::AudioProcessorEditor* createEditor() override; 47 | bool hasEditor() const override; 48 | 49 | //============================================================================== 50 | const juce::String getName() const override; 51 | 52 | bool acceptsMidi() const override; 53 | bool producesMidi() const override; 54 | bool isMidiEffect() const override; 55 | double getTailLengthSeconds() const override; 56 | 57 | //============================================================================== 58 | int getNumPrograms() override; 59 | int getCurrentProgram() override; 60 | void setCurrentProgram (int index) override; 61 | const juce::String getProgramName (int index) override; 62 | void changeProgramName (int index, const juce::String& newName) override; 63 | 64 | //============================================================================== 65 | void getStateInformation (juce::MemoryBlock& destData) override; 66 | void setStateInformation (const void* data, int sizeInBytes) override; 67 | 68 | private: 69 | //============================================================================== 70 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ExampleAudioProcessor) 71 | void parameterChanged (const juce::String& param, float value) override; 72 | 73 | protected: 74 | juce::AudioProcessorValueTreeState vts; 75 | foleys::MagicProcessorState magicState { *this }; 76 | 77 | foleys::MagicPlotSource* oscilloscope = nullptr; 78 | 79 | std::atomic* ioGain; 80 | std::atomic* ioReductionMix; 81 | std::atomic* ioWarpMix; 82 | 83 | std::atomic* controlReduction; 84 | std::atomic* controlWarp; 85 | 86 | std::atomic* filterCutoff; 87 | std::atomic* filterResonance; 88 | 89 | // toggles 90 | enum class FilterPlacement { 91 | Off, Pre, Post 92 | } filterPlacement = FilterPlacement::Pre; 93 | 94 | float outPeak = 0.f; 95 | 96 | // juce::Label outPeakLabel; 97 | 98 | private: 99 | BitReduction bitReduction; 100 | WaveWarp waveWarp; 101 | BiquadFilter biquadFilter; 102 | 103 | std::shared_ptr mainLNF; 104 | }; 105 | -------------------------------------------------------------------------------- /src/dsp/BiquadFilter.cpp: -------------------------------------------------------------------------------- 1 | #include "BiquadFilter.hpp" 2 | 3 | #define _USE_MATH_DEFINES 4 | #include 5 | 6 | BiquadFilter::BiquadFilter(float freq, float res) 7 | : mF(freq), 8 | mQ(res), 9 | mDbGain(0), 10 | mType(BiquadFilter::FilterType::LowPass), 11 | mSampleRate(48000) { 12 | 13 | clear(); 14 | this->coeff[0].updateFilterCoeff(this->mType, mF, mQ, mDbGain, this->mSampleRate); 15 | this->coeff[1].updateFilterCoeff(this->mType, mF, mQ, mDbGain, this->mSampleRate); 16 | } 17 | 18 | void BiquadFilter::clear() 19 | { 20 | this->coeff[0].mZ1 = 0; 21 | this->coeff[0].mZ2 = 0; 22 | this->coeff[1].mZ1 = 0; 23 | this->coeff[1].mZ2 = 0; 24 | } 25 | 26 | void BiquadFilter::setFilterType (BiquadFilter::FilterType type) 27 | { 28 | if (type != mType) 29 | { 30 | mType = type; 31 | clear(); 32 | } 33 | } 34 | 35 | void BiquadFilter::setFrequency(double f) { 36 | 37 | if (mF != f) { 38 | mF = static_cast(f); 39 | this->updateCoefficients(); 40 | } 41 | } 42 | 43 | void BiquadFilter::setResonance(double q) { 44 | 45 | if (mQ != q) { 46 | mQ = static_cast(q); 47 | this->updateCoefficients(); 48 | } 49 | } 50 | 51 | void BiquadFilter::Coefficients::updateFilterCoeff(FilterType type, float freq, float res, float dbGain, double sampleRate) 52 | { 53 | if (freq <= 0 || freq != freq) 54 | { 55 | mA0 = 1; 56 | mA1 = 0; 57 | mA2 = 0; 58 | mB1 = 0; 59 | mB2 = 0; 60 | return; 61 | } 62 | 63 | double norm; 64 | double V = pow (10, fabs (dbGain) / 20.0); 65 | double K = tan (M_PI * (freq / sampleRate)); 66 | 67 | switch (type) 68 | { 69 | case FilterType::LowPass: 70 | norm = 1 / (1 + K / res + K * K); 71 | mA0 = K * K * norm; 72 | mA1 = 2 * mA0; 73 | mA2 = mA0; 74 | mB1 = 2 * (K * K - 1) * norm; 75 | mB2 = (1 - K / res + K * K) * norm; 76 | break; 77 | 78 | case FilterType::HighPass: 79 | norm = 1 / (1 + K / res + K * K); 80 | mA0 = 1 * norm; 81 | mA1 = -2 * mA0; 82 | mA2 = mA0; 83 | mB1 = 2 * (K * K - 1) * norm; 84 | mB2 = (1 - K / res + K * K) * norm; 85 | break; 86 | 87 | case FilterType::BandPass: 88 | norm = 1 / (1 + K / res + K * K); 89 | mA0 = K / res * norm; 90 | mA1 = 0; 91 | mA2 = -mA0; 92 | mB1 = 2 * (K * K - 1) * norm; 93 | mB2 = (1 - K / res + K * K) * norm; 94 | break; 95 | 96 | case FilterType::Notch: 97 | norm = 1 / (1 + K / res + K * K); 98 | mA0 = (1 + K * K) * norm; 99 | mA1 = 2 * (K * K - 1) * norm; 100 | mA2 = mA0; 101 | mB1 = mA1; 102 | mB2 = (1 - K / res + K * K) * norm; 103 | break; 104 | 105 | case FilterType::Peak: 106 | 107 | if (dbGain >= 0) 108 | { // boost 109 | norm = 1 / (1 + K / res + K * K); 110 | mA0 = (1 + K / res * V + K * K) * norm; 111 | mA1 = 2 * (K * K - 1) * norm; 112 | mA2 = (1 - K / res * V + K * K) * norm; 113 | mB1 = mA1; 114 | mB2 = (1 - K / res + K * K) * norm; 115 | } 116 | else 117 | { // cut 118 | norm = 1 / (1 + K / res * V + K * K); 119 | mA0 = (1 + K / res + K * K) * norm; 120 | mA1 = 2 * (K * K - 1) * norm; 121 | mA2 = (1 - K / res + K * K) * norm; 122 | mB1 = mA1; 123 | mB2 = (1 - K / res * V + K * K) * norm; 124 | } 125 | break; 126 | 127 | case FilterType::LowShelf: 128 | if (dbGain >= 0) 129 | { // boost 130 | norm = 1 / (1 + K / res + K * K); 131 | mA0 = (1 + sqrt (V) * K / res + V * K * K) * norm; 132 | mA1 = 2 * (V * K * K - 1) * norm; 133 | mA2 = (1 - sqrt (V) * K / res + V * K * K) * norm; 134 | mB1 = 2 * (K * K - 1) * norm; 135 | mB2 = (1 - K / res + K * K) * norm; 136 | } 137 | else 138 | { // cut 139 | norm = 1 / (1 + sqrt (V) * K / res + V * K * K); 140 | mA0 = (1 + K / res + K * K) * norm; 141 | mA1 = 2 * (K * K - 1) * norm; 142 | mA2 = (1 - K / res + K * K) * norm; 143 | mB1 = 2 * (V * K * K - 1) * norm; 144 | mB2 = (1 - sqrt (V) * K / res + V * K * K) * norm; 145 | } 146 | break; 147 | 148 | case FilterType::HighShelf: 149 | if (dbGain >= 0) 150 | { // boost 151 | norm = 1 / (1 + K / res + K * K); 152 | mA0 = (V + sqrt (V) * K / res + K * K) * norm; 153 | mA1 = 2 * (K * K - V) * norm; 154 | mA2 = (V - sqrt (V) * K / res + K * K) * norm; 155 | mB1 = 2 * (K * K - 1) * norm; 156 | mB2 = (1 - K / res + K * K) * norm; 157 | } 158 | else 159 | { // cut 160 | norm = 1 / (V + sqrt (V) * K / res + K * K); 161 | mA0 = (1 + K / res + K * K) * norm; 162 | mA1 = 2 * (K * K - 1) * norm; 163 | mA2 = (1 - K / res + K * K) * norm; 164 | mB1 = 2 * (K * K - V) * norm; 165 | mB2 = (V - sqrt (V) * K / res + K * K) * norm; 166 | } 167 | break; 168 | 169 | case FilterType::LowShelfNoQ: 170 | if (dbGain >= 0) 171 | { // boost 172 | norm = 1 / (1 + sqrt (2) * K + K * K); 173 | mA0 = (1 + sqrt (2 * V) * K + V * K * K) * norm; 174 | mA1 = 2 * (V * K * K - 1) * norm; 175 | mA2 = (1 - sqrt (2 * V) * K + V * K * K) * norm; 176 | mB1 = 2 * (K * K - 1) * norm; 177 | mB2 = (1 - sqrt (2) * K + K * K) * norm; 178 | } 179 | else 180 | { // cut 181 | norm = 1 / (1 + sqrt (2 * V) * K + V * K * K); 182 | mA0 = (1 + sqrt (2) * K + K * K) * norm; 183 | mA1 = 2 * (K * K - 1) * norm; 184 | mA2 = (1 - sqrt (2) * K + K * K) * norm; 185 | mB1 = 2 * (V * K * K - 1) * norm; 186 | mB2 = (1 - sqrt (2 * V) * K + V * K * K) * norm; 187 | } 188 | break; 189 | 190 | case FilterType::HighShelfNoQ: 191 | if (dbGain >= 0) 192 | { // boost 193 | norm = 1 / (1 + sqrt (2) * K + K * K); 194 | mA0 = (V + sqrt (2 * V) * K + K * K) * norm; 195 | mA1 = 2 * (K * K - V) * norm; 196 | mA2 = (V - sqrt (2 * V) * K + K * K) * norm; 197 | mB1 = 2 * (K * K - 1) * norm; 198 | mB2 = (1 - sqrt (2) * K + K * K) * norm; 199 | } 200 | else 201 | { // cut 202 | norm = 1 / (V + sqrt (2 * V) * K + K * K); 203 | mA0 = (1 + sqrt (2) * K + K * K) * norm; 204 | mA1 = 2 * (K * K - 1) * norm; 205 | mA2 = (1 - sqrt (2) * K + K * K) * norm; 206 | mB1 = 2 * (K * K - V) * norm; 207 | mB2 = (V - sqrt (2 * V) * K + K * K) * norm; 208 | } 209 | break; 210 | 211 | case FilterType::AllPass: 212 | norm = 1 / (1 + K / res + K * K); 213 | mA0 = (1 - K / res + K * K) * norm; 214 | mA1 = 2 * (K * K - 1) * norm; 215 | mA2 = 1; 216 | mB1 = mA1; 217 | mB2 = mA0; 218 | break; 219 | 220 | case FilterType::Off: 221 | mA0 = 1; 222 | mA1 = 0; 223 | mA2 = 0; 224 | mB1 = 0; 225 | mB2 = 0; 226 | break; 227 | } 228 | } 229 | 230 | //void BiquadFilter::copyCoeffFrom (BiquadFilter& other) 231 | //{ 232 | // mA0 = other.mA0; 233 | // mA1 = other.mA1; 234 | // mA2 = other.mA2; 235 | // mB1 = other.mB1; 236 | // mB2 = other.mB2; 237 | //} 238 | 239 | float BiquadFilter::Coefficients::getMagnitudeResponseAt (float f, double sampleRate) const 240 | { 241 | auto const piw0 = (f / sampleRate) * M_PI * 2; 242 | auto const cosw = std::cos (piw0); 243 | auto const sinw = std::sin (piw0); 244 | 245 | auto square = [] (auto z) { 246 | return z * z; 247 | }; 248 | 249 | auto const numerator = sqrt (square (mA0 * square (cosw) - mA0 * square (sinw) + mA1 * cosw + mA2) + square (2 * mA0 * cosw * sinw + mA1 * (sinw))); 250 | auto const denominator = sqrt (square (square (cosw) - square (sinw) + mB1 * cosw + mB2) + square (2 * cosw * sinw + mB1 * (sinw))); 251 | 252 | return static_cast (numerator / denominator); 253 | } 254 | 255 | void BiquadFilter::updateCoefficients() { 256 | this->coeff[0].updateFilterCoeff(this->mType, mF, mQ, mDbGain, this->mSampleRate); 257 | this->coeff[1].updateFilterCoeff(this->mType, mF, mQ, mDbGain, this->mSampleRate); 258 | } 259 | -------------------------------------------------------------------------------- /src/dsp/BiquadFilter.hpp: -------------------------------------------------------------------------------- 1 | #ifndef HANDYOSC_DSP_BIQUAD_FILTER_HPP 2 | #define HANDYOSC_DSP_BIQUAD_FILTER_HPP 3 | 4 | #include "JuceHeader.h" 5 | 6 | class BiquadFilter 7 | { 8 | public: 9 | BiquadFilter() = default; 10 | BiquadFilter(float freq, float res); 11 | 12 | enum class FilterType 13 | { 14 | Off, 15 | LowPass, 16 | HighPass, 17 | BandPass, 18 | Notch, 19 | Peak, 20 | LowShelf, 21 | HighShelf, 22 | LowShelfNoQ, 23 | HighShelfNoQ, 24 | AllPass 25 | }; 26 | 27 | template 28 | void process (const ProcessContext& context) noexcept 29 | { 30 | auto& outputBlock = context.getOutputBlock(); 31 | 32 | const auto numOutputChannels = outputBlock.getNumChannels(); 33 | const auto numSamples = outputBlock.getNumSamples(); 34 | 35 | jassert (outputBlock.getNumSamples() == numSamples); 36 | juce::ignoreUnused (numSamples); 37 | 38 | if (numOutputChannels != 2 || numOutputChannels == 0 || numOutputChannels > 2) 39 | return; 40 | 41 | if (context.isBypassed) 42 | return; 43 | 44 | for (size_t channel = 0; channel < numOutputChannels; ++channel) 45 | { 46 | auto* outputSamples = outputBlock.getChannelPointer (channel); 47 | 48 | for (size_t i = 0; i < numSamples; ++i) 49 | { 50 | outputSamples[i] = this->filter(outputSamples[i], channel); 51 | } 52 | } 53 | } 54 | 55 | void setSampleRate (double sampleRate) { mSampleRate = sampleRate; } 56 | void clear(); 57 | 58 | void setFilterType (FilterType type); 59 | 60 | void setFrequency(double f); 61 | void setResonance(double q); 62 | 63 | // void copyCoeffFrom (BiquadFilter& other); 64 | bool usesGain() const { return mType == FilterType::Peak || mType == FilterType::HighShelf || mType == FilterType::LowShelf; } 65 | bool usesQ() { return true; } 66 | 67 | inline float filter (float in, int channel) { 68 | double out = in * this->coeff[channel].mA0 + this->coeff[channel].mZ1; 69 | this->coeff[channel].mZ1 = in * this->coeff[channel].mA1 + this->coeff[channel].mZ2 - this->coeff[channel].mB1 * out; 70 | this->coeff[channel].mZ2 = in * this->coeff[channel].mA2 - this->coeff[channel].mB2 * out; 71 | return static_cast(out); 72 | } 73 | 74 | float mF; 75 | float mQ; 76 | float mDbGain; 77 | FilterType mType; 78 | 79 | protected: 80 | 81 | double mSampleRate; 82 | 83 | struct Coefficients { 84 | double mA0; 85 | double mA1; 86 | double mA2; 87 | double mB1; 88 | double mB2; 89 | double mZ1; 90 | double mZ2; 91 | 92 | void updateFilterCoeff(FilterType type, float freq, float res, float dbGain, double sampleRate); 93 | float getMagnitudeResponseAt (float f, double sampleRate) const; 94 | 95 | } coeff[2] = {}; 96 | 97 | void updateCoefficients(); 98 | }; 99 | 100 | #endif // HANDYOSC_DSP_BIQUAD_FILTER_HPP -------------------------------------------------------------------------------- /src/dsp/BitReduction.cpp: -------------------------------------------------------------------------------- 1 | #include "BitReduction.hpp" 2 | -------------------------------------------------------------------------------- /src/dsp/BitReduction.hpp: -------------------------------------------------------------------------------- 1 | 2 | #ifndef HANDYOSC_BIT_REDUCTION_HPP 3 | #define HANDYOSC_BIT_REDUCTION_HPP 4 | 5 | #include "JuceHeader.h" 6 | 7 | inline float drywet(float dry, float wet, float mix) { 8 | return (wet * mix) + (dry * (1 - mix)); 9 | } 10 | 11 | class BitReduction { 12 | public: 13 | BitReduction() = default; 14 | 15 | template 16 | void process(const ProcessContext& context, float amount) noexcept { 17 | const auto& inputBlock = context.getInputBlock(); 18 | auto& outputBlock = context.getOutputBlock(); 19 | 20 | const auto numInputChannels = inputBlock.getNumChannels(); 21 | const auto numOutputChannels = outputBlock.getNumChannels(); 22 | const auto numSamples = outputBlock.getNumSamples(); 23 | 24 | jassert (inputBlock.getNumSamples() == numSamples); 25 | juce::ignoreUnused (numSamples); 26 | 27 | if (numOutputChannels != 2 || numInputChannels == 0 || numInputChannels > 2) 28 | return; 29 | 30 | if (numInputChannels == 2) 31 | { 32 | outputBlock.copyFrom (inputBlock); 33 | } 34 | else 35 | { 36 | outputBlock.getSingleChannelBlock (0).copyFrom (inputBlock); 37 | outputBlock.getSingleChannelBlock (1).copyFrom (inputBlock); 38 | } 39 | 40 | if (context.isBypassed) 41 | return; 42 | 43 | for (size_t channel = 0; channel < numInputChannels; ++channel) 44 | { 45 | auto* inputSamples = inputBlock.getChannelPointer (channel); 46 | auto* outputSamples = outputBlock.getChannelPointer (channel); 47 | 48 | for (size_t i = 0; i < numSamples; ++i) { 49 | outputSamples[i] = this->mix(inputSamples[i], 50 | processSample (inputSamples[i], amount) 51 | ); 52 | } 53 | } 54 | } 55 | 56 | void setMixAmount(float amount) { this->mixAmount = amount; } 57 | 58 | protected: 59 | inline float processSample(float x, float amount) noexcept { 60 | return static_cast(static_cast(x * amount) / amount); 61 | } 62 | 63 | inline float mix(float dry, float wet) const noexcept { 64 | return (wet * this->mixAmount) + (dry * (1 - this->mixAmount)); 65 | } 66 | 67 | float mixAmount = 1.f; 68 | }; 69 | 70 | #endif // HANDYOSC_BIT_REDUCTION_HPP -------------------------------------------------------------------------------- /src/dsp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file (GLOB FILES_DSP CONFIGURE_DEPENDS 2 | "*.hpp" "*.cpp" 3 | ) 4 | 5 | target_sources(${CMAKE_PROJECT_NAME} 6 | PRIVATE 7 | ${FILES_DSP} 8 | ) -------------------------------------------------------------------------------- /src/dsp/WaveWarp.hpp: -------------------------------------------------------------------------------- 1 | #ifndef HANDYOSC_WAVE_WARP_HPP 2 | #define HANDYOSC_WAVE_WARP_HPP 3 | 4 | #include "JuceHeader.h" 5 | 6 | class WaveWarp { 7 | public: 8 | WaveWarp() = default; 9 | 10 | template 11 | void process(const ProcessContext& context, float amount) noexcept { 12 | 13 | auto& outputBlock = context.getOutputBlock(); 14 | 15 | const auto numOutputChannels = outputBlock.getNumChannels(); 16 | const auto numSamples = outputBlock.getNumSamples(); 17 | 18 | jassert (outputBlock.getNumSamples() == numSamples); 19 | juce::ignoreUnused (numSamples); 20 | 21 | if (numOutputChannels != 2 || numOutputChannels == 0 || numOutputChannels > 2) 22 | return; 23 | 24 | if (context.isBypassed) 25 | return; 26 | 27 | switch (juce::roundToInt(this->type->load())) { 28 | 29 | case 0: 30 | 31 | for (size_t channel = 0; channel < numOutputChannels; ++channel) { 32 | auto* outputSamples = outputBlock.getChannelPointer (channel); 33 | for (size_t i = 0; i < numSamples; ++i) { 34 | 35 | if (std::abs(outputSamples[i]) > std::abs(this->floatSample[channel])) { 36 | this->floatSample[channel] = outputSamples[i]; 37 | } 38 | else { 39 | if (this->floatSample[channel] > 0) 40 | this->floatSample[channel] -= amount; 41 | else if (this->floatSample[channel] < 0) 42 | this->floatSample[channel] += amount; 43 | } 44 | 45 | outputSamples[i] = this->mix(outputSamples[i], this->floatSample[channel]); 46 | } 47 | } 48 | 49 | break; 50 | case 1: 51 | 52 | for (size_t channel = 0; channel < numOutputChannels; ++channel) { 53 | auto* outputSamples = outputBlock.getChannelPointer (channel); 54 | for (size_t i = 0; i < numSamples; ++i) { 55 | 56 | if (outputSamples[i] > std::abs(this->floatSample[channel])) { 57 | this->floatSample[channel] = outputSamples[i]; 58 | } 59 | else { 60 | if (this->floatSample[channel] > 0) 61 | this->floatSample[channel] -= amount; 62 | else if (this->floatSample[channel] < 0) 63 | this->floatSample[channel] += amount; 64 | } 65 | 66 | outputSamples[i] = this->mix(outputSamples[i], this->floatSample[channel]); 67 | } 68 | } 69 | 70 | break; 71 | case 2: 72 | 73 | for (size_t channel = 0; channel < numOutputChannels; ++channel) { 74 | auto* outputSamples = outputBlock.getChannelPointer (channel); 75 | for (size_t i = 0; i < numSamples; ++i) { 76 | 77 | if (outputSamples[i] > this->floatSample[channel]) { 78 | this->floatSample[channel] = outputSamples[i]; 79 | } 80 | else { 81 | if (this->floatSample[channel] > 0) 82 | this->floatSample[channel] -= amount; 83 | else if (this->floatSample[channel] < 0) 84 | this->floatSample[channel] += amount; 85 | } 86 | 87 | outputSamples[i] = this->mix(outputSamples[i], this->floatSample[channel]); 88 | } 89 | } 90 | 91 | break; 92 | 93 | default: 94 | break; 95 | } 96 | } 97 | 98 | void setMixAmount(float amount) { this->mixAmount = amount; } 99 | 100 | std::atomic* type; 101 | 102 | private: 103 | 104 | [[nodiscard]] inline float mix(float dry, float wet) const noexcept { 105 | return (wet * this->mixAmount) + (dry * (1 - this->mixAmount)); 106 | } 107 | 108 | float floatSample[2] = { 0.f }; 109 | float mixAmount = 1.f; 110 | }; 111 | #endif // HANDYOSC_WAVE_WARP_HPP -------------------------------------------------------------------------------- /src/gui/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file (GLOB FILES_GUI CONFIGURE_DEPENDS 2 | "*.hpp" "*.cpp" 3 | "item/*.hpp" "item/*.cpp" 4 | "lnf/*.hpp" "lnf/*.cpp" 5 | ) 6 | 7 | target_sources(${CMAKE_PROJECT_NAME} 8 | PRIVATE 9 | ${FILES_GUI} 10 | ) -------------------------------------------------------------------------------- /src/gui/item/DynText.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "JuceHeader.h" 3 | #include "foleys_gui_magic/foleys_gui_magic.h" 4 | 5 | class DynTextItem : public foleys::GuiItem 6 | { 7 | public: 8 | FOLEYS_DECLARE_GUI_FACTORY (DynTextItem) 9 | 10 | DynTextItem(foleys::MagicGUIBuilder& builder, const juce::ValueTree& node) 11 | : foleys::GuiItem (builder, node) { 12 | 13 | const auto textColor = node.getProperty("color"); 14 | const auto fontSize = node.getProperty("font-size"); 15 | const auto initText = node.getProperty("text"); 16 | 17 | component = std::make_unique(); 18 | addAndMakeVisible(component.get()); 19 | } 20 | 21 | ~DynTextItem() {} 22 | 23 | Component* getWrappedComponent() override { 24 | return this->component.get(); 25 | } 26 | 27 | void update() override {} 28 | 29 | protected: 30 | 31 | class DynText : public juce::Component { 32 | public: 33 | DynText() {} 34 | ~DynText() {} 35 | 36 | void paint(Graphics& g) override { 37 | auto bounds = Rectangle (0, 0, getWidth(), getHeight()).toFloat(); 38 | g.setColour (Colours::white); 39 | g.drawFittedText (text, bounds.toNearestInt(), Justification::centred, 1); 40 | } 41 | 42 | void resized() override {} 43 | void refreshStates() {} 44 | void setText(juce::String text) { this->text.swapWith(text); } 45 | 46 | protected: 47 | juce::String text{"Peak: -inf Db"}; 48 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DynText) 49 | }; 50 | 51 | std::unique_ptr component; 52 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DynTextItem) 53 | }; 54 | 55 | -------------------------------------------------------------------------------- /src/gui/lnf/MainLNF.cpp: -------------------------------------------------------------------------------- 1 | #include "MainLNF.hpp" 2 | 3 | MainLNF::MainLNF() { 4 | roboto = juce::Typeface::createSystemTypefaceFor (BinaryData::RobotoCondensedRegular_ttf, 5 | BinaryData::RobotoCondensedRegular_ttfSize); 6 | 7 | robotoBold = juce::Typeface::createSystemTypefaceFor (BinaryData::RobotoCondensedBold_ttf, 8 | BinaryData::RobotoCondensedBold_ttfSize); 9 | } 10 | 11 | juce::Typeface::Ptr MainLNF::getTypefaceForFont (const juce::Font& font) { 12 | return font.isBold() ? robotoBold : roboto; 13 | } 14 | 15 | /** Positions ComboBox text to leave room for Label */ 16 | void MainLNF::positionComboBoxText (juce::ComboBox& box, juce::Label& label) { 17 | juce::LookAndFeel_V4::positionComboBoxText (box, label); 18 | label.setFont (label.getFont().boldened()); 19 | label.setJustificationType (juce::Justification::centred); 20 | } 21 | 22 | /** Creates tabbed layout for tabbed components */ 23 | void MainLNF::createTabTextLayout ( 24 | const juce::TabBarButton& button, 25 | float length, 26 | float depth, 27 | juce::Colour colour, 28 | juce::TextLayout& textLayout 29 | ) { 30 | juce::Font font (depth * 0.45f, juce::Font::bold); 31 | font.setUnderline (button.hasKeyboardFocus (false)); 32 | 33 | juce::AttributedString s; 34 | s.setJustification (juce::Justification::centred); 35 | s.append (button.getButtonText().trim(), font, colour); 36 | 37 | textLayout.createLayout (s, length); 38 | } 39 | 40 | /** Finds the proper parent component for a popup menu with these options */ 41 | juce::Component* MainLNF::getParentComponentForMenuOptions (const juce::PopupMenu::Options& options) { 42 | #if JUCE_IOS 43 | if (juce::PluginHostType::getPluginLoadedAs() == juce::AudioProcessor::wrapperType_AudioUnitv3) 44 | { 45 | if (options.getParentComponent() == nullptr && options.getTargetComponent() != nullptr) 46 | return options.getTargetComponent()->getTopLevelComponent(); 47 | } 48 | #endif 49 | return juce::LookAndFeel_V2::getParentComponentForMenuOptions (options); 50 | } -------------------------------------------------------------------------------- /src/gui/lnf/MainLNF.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "JuceHeader.h" 3 | #include "BinaryData.h" 4 | 5 | class MainLNF : public juce::LookAndFeel_V4 { 6 | public: 7 | MainLNF(); 8 | 9 | juce::Typeface::Ptr getTypefaceForFont (const juce::Font&) override; 10 | 11 | /** Positions ComboBox text to leave room for Label */ 12 | void positionComboBoxText (juce::ComboBox& box, juce::Label& label) override; 13 | 14 | /** Creates tabbed layout for tabbed components */ 15 | void createTabTextLayout ( 16 | const juce::TabBarButton& button, 17 | float length, 18 | float depth, 19 | juce::Colour colour, 20 | juce::TextLayout& textLayout); 21 | 22 | /** Finds the proper parent component for a popup menu with these options */ 23 | juce::Component* getParentComponentForMenuOptions (const juce::PopupMenu::Options& options) override; 24 | 25 | protected: 26 | std::unique_ptr knob = juce::Drawable::createFromImageData ( 27 | BinaryData::knob_svg, BinaryData::knob_svgSize); 28 | 29 | std::unique_ptr pointer = juce::Drawable::createFromImageData ( 30 | BinaryData::pointer_svg, BinaryData::pointer_svgSize); 31 | 32 | juce::Typeface::Ptr roboto; 33 | juce::Typeface::Ptr robotoBold; 34 | 35 | private: 36 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainLNF) 37 | }; -------------------------------------------------------------------------------- /src/pch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | --------------------------------------------------------------------------------