├── res ├── images │ ├── mv_panels.jpg │ ├── chorus_panels.jpg │ ├── golem_panels.jpg │ ├── holt_panels.jpg │ ├── hombre_panels.jpg │ ├── rasp_panels.jpg │ ├── reseq_panels.jpg │ ├── tape_panels.jpg │ ├── console_panels.jpg │ ├── distance_panels.jpg │ ├── dual_bsg_panels.jpg │ ├── tremolo_panels.jpg │ ├── vibrato_panels.jpg │ ├── capacitor_panels.jpg │ ├── interstage_panels.jpg │ ├── monitoring_panels.jpg │ └── rackwindows_panels.jpg └── components │ ├── rw_knob_trimpot.svg │ ├── rw_knob_medium_dark.svg │ ├── rw_knob_small_dark.svg │ ├── rw_knob_large_dark.svg │ ├── rw_switch_three_vert_0.svg │ ├── rw_switch_three_vert_2.svg │ ├── rw_switch_three_0.svg │ ├── rw_switch_three_2.svg │ ├── rw_switch_three_1.svg │ ├── rw_switch_three_vert_1.svg │ ├── rw_PJ301M.svg │ ├── rw_PJ301M_silver.svg │ ├── rw_screw_dark.svg │ ├── rw_CKSS_0.svg │ ├── rw_CKSS_rot_0.svg │ ├── rw_CKSS_1.svg │ └── rw_CKSS_rot_1.svg ├── .gitignore ├── .github ├── workflows │ ├── buildDevelop.yml │ └── buildRelease.yml └── actions │ ├── build_linux │ ├── entrypoint.sh │ └── Dockerfile │ ├── build_win │ ├── entrypoint.sh │ └── Dockerfile │ ├── build_osx │ ├── entrypoint.sh │ └── Dockerfile │ ├── upload_zip │ └── script.sh │ └── combine_zip │ └── script.sh ├── Makefile ├── changelog.md ├── LICENSE.md ├── src ├── plugin.hpp ├── components.hpp ├── monitoring.cpp ├── tape.cpp ├── interstage.cpp ├── bitshiftgain.cpp ├── hombre.cpp ├── distance.cpp ├── plugin.cpp ├── rasp.cpp ├── tremolo.cpp ├── holt.cpp └── vibrato.cpp ├── plugin.json └── readme.md /res/images/mv_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/mv_panels.jpg -------------------------------------------------------------------------------- /res/images/chorus_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/chorus_panels.jpg -------------------------------------------------------------------------------- /res/images/golem_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/golem_panels.jpg -------------------------------------------------------------------------------- /res/images/holt_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/holt_panels.jpg -------------------------------------------------------------------------------- /res/images/hombre_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/hombre_panels.jpg -------------------------------------------------------------------------------- /res/images/rasp_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/rasp_panels.jpg -------------------------------------------------------------------------------- /res/images/reseq_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/reseq_panels.jpg -------------------------------------------------------------------------------- /res/images/tape_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/tape_panels.jpg -------------------------------------------------------------------------------- /res/images/console_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/console_panels.jpg -------------------------------------------------------------------------------- /res/images/distance_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/distance_panels.jpg -------------------------------------------------------------------------------- /res/images/dual_bsg_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/dual_bsg_panels.jpg -------------------------------------------------------------------------------- /res/images/tremolo_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/tremolo_panels.jpg -------------------------------------------------------------------------------- /res/images/vibrato_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/vibrato_panels.jpg -------------------------------------------------------------------------------- /res/images/capacitor_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/capacitor_panels.jpg -------------------------------------------------------------------------------- /res/images/interstage_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/interstage_panels.jpg -------------------------------------------------------------------------------- /res/images/monitoring_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/monitoring_panels.jpg -------------------------------------------------------------------------------- /res/images/rackwindows_panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0jo/rackwindows/HEAD/res/images/rackwindows_panels.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /design 3 | /dist 4 | /plugin.so 5 | /plugin.dylib 6 | /plugin.dll 7 | /todo.md 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.github/workflows/buildDevelop.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - develop 5 | name: Develop 6 | env: 7 | RACK_SDK_VERSION: 1.1.6 8 | jobs: 9 | buildLinux: 10 | name: Build Linux 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Build Linux 15 | uses: ./.github/actions/build_linux 16 | -------------------------------------------------------------------------------- /.github/actions/build_linux/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | export RACK_DIR=${GITHUB_WORKSPACE}/Rack-SDK 6 | export RACK_USER_DIR=${GITHUB_WORKSPACE} 7 | 8 | git submodule update --init --recursive 9 | 10 | curl -L https://vcvrack.com/downloads/Rack-SDK-${RACK_SDK_VERSION}.zip -o rack-sdk.zip 11 | unzip -o rack-sdk.zip 12 | rm rack-sdk.zip 13 | 14 | make clean 15 | make dist 16 | -------------------------------------------------------------------------------- /.github/actions/build_win/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | export RACK_DIR=${GITHUB_WORKSPACE}/Rack-SDK 6 | export RACK_USER_DIR=${GITHUB_WORKSPACE} 7 | 8 | export CC=x86_64-w64-mingw32-gcc-posix 9 | export CXX=x86_64-w64-mingw32-g++-posix 10 | export STRIP=x86_64-w64-mingw32-strip 11 | 12 | git submodule update --init --recursive 13 | 14 | curl -L https://vcvrack.com/downloads/Rack-SDK-${RACK_SDK_VERSION}.zip -o rack-sdk.zip 15 | unzip -o rack-sdk.zip 16 | rm rack-sdk.zip 17 | 18 | make clean 19 | make dist 20 | -------------------------------------------------------------------------------- /.github/actions/build_osx/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | export RACK_DIR=${GITHUB_WORKSPACE}/Rack-SDK 6 | export RACK_USER_DIR=${GITHUB_WORKSPACE} 7 | 8 | export CC=x86_64-apple-darwin15-clang 9 | export CXX=x86_64-apple-darwin15-clang++ 10 | export STRIP=x86_64-apple-darwin15-strip 11 | 12 | git submodule update --init --recursive 13 | 14 | curl -L https://vcvrack.com/downloads/Rack-SDK-${RACK_SDK_VERSION}.zip -o rack-sdk.zip 15 | unzip -o rack-sdk.zip 16 | rm rack-sdk.zip 17 | 18 | make clean 19 | make dist 20 | -------------------------------------------------------------------------------- /.github/actions/build_linux/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | LABEL "com.github.actions.name"="VCVRackPluginBuilder-Linux" 4 | LABEL "com.github.actions.description"="Builds a VCV Rack plugin for Linux" 5 | LABEL "com.github.actions.icon"="headphones" 6 | LABEL "com.github.actions.color"="purple" 7 | 8 | LABEL "repository"="TBD" 9 | LABEL "homepage"="TBD" 10 | LABEL "maintainer"="n0jo" 11 | 12 | RUN apt-get update 13 | RUN apt-get install -y build-essential cmake curl gcc g++ git make tar unzip zip libgl1-mesa-dev libglu1-mesa-dev jq 14 | 15 | ADD entrypoint.sh /entrypoint.sh 16 | RUN chmod a+x /entrypoint.sh 17 | 18 | ENTRYPOINT ["/entrypoint.sh"] 19 | -------------------------------------------------------------------------------- /.github/actions/build_win/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stretch 2 | 3 | LABEL "com.github.actions.name"="VCVRackPluginBuilder-Windows" 4 | LABEL "com.github.actions.description"="Builds a VCV Rack plugin for Windows" 5 | LABEL "com.github.actions.icon"="headphones" 6 | LABEL "com.github.actions.color"="purple" 7 | 8 | LABEL "repository"="TBD" 9 | LABEL "homepage"="TBD" 10 | LABEL "maintainer"="n0jo" 11 | 12 | RUN apt-get update 13 | RUN apt-get install -y build-essential cmake curl gcc g++ git make tar unzip zip libgl1-mesa-dev libglu1-mesa-dev jq g++-mingw-w64-x86-64 14 | 15 | ADD entrypoint.sh /entrypoint.sh 16 | RUN chmod a+x /entrypoint.sh 17 | 18 | ENTRYPOINT ["/entrypoint.sh"] 19 | -------------------------------------------------------------------------------- /.github/actions/upload_zip/script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | GITHUB_API_URL=https://api.github.com 6 | 7 | GITHUB_TOKEN=$1 8 | 9 | # Get release url 10 | curl -o release.json \ 11 | --header "Authorization: token ${GITHUB_TOKEN}" \ 12 | --request GET \ 13 | ${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${GITHUB_REF#"refs/"} 14 | 15 | UPLOAD_URL=$(jq -r .upload_url release.json) 16 | 17 | ASSET_PATH=$(ls dist/*.zip) 18 | 19 | curl -i \ 20 | --header "Authorization: token ${GITHUB_TOKEN}" \ 21 | --header "Content-Type: application/zip" \ 22 | --request POST \ 23 | --data-binary @"${ASSET_PATH}" \ 24 | ${UPLOAD_URL%"{?name,label\}"}?name=${ASSET_PATH#"dist/"} 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # If RACK_DIR is not defined when calling the Makefile, default to two directories above 2 | RACK_DIR ?= ../.. 3 | 4 | # FLAGS will be passed to both the C and C++ compiler 5 | FLAGS += 6 | CFLAGS += 7 | CXXFLAGS += 8 | 9 | # Careful about linking to shared libraries, since you can't assume much about the user's environment and library search path. 10 | # Static libraries are fine, but they should be added to this plugin's build system. 11 | LDFLAGS += 12 | 13 | # Add .cpp files to the build 14 | SOURCES += $(wildcard src/*.cpp) 15 | 16 | # Add files to the ZIP package when running `make dist` 17 | # The compiled plugin and "plugin.json" are automatically added. 18 | DISTRIBUTABLES += res 19 | # DISTRIBUTABLES += $(wildcard LICENSE*) 20 | 21 | # Include the Rack plugin Makefile framework 22 | include $(RACK_DIR)/plugin.mk 23 | -------------------------------------------------------------------------------- /res/components/rw_knob_trimpot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /res/components/rw_knob_medium_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /res/components/rw_knob_small_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /res/components/rw_knob_large_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/actions/combine_zip/script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | GITHUB_API_URL=https://api.github.com 6 | 7 | GITHUB_TOKEN=$1 8 | 9 | curl -o release.json \ 10 | --header "Authorization: token ${GITHUB_TOKEN}" \ 11 | --request GET \ 12 | ${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${GITHUB_REF#"refs/"} 13 | 14 | UPLOAD_URL=$(jq -r .upload_url release.json) 15 | 16 | DOWNLOAD_LIST=$(jq -r ".assets | .[] | .url" release.json) 17 | 18 | rm release.json 19 | 20 | mkdir dist 21 | 22 | echo "$DOWNLOAD_LIST" | while IFS= read -r line 23 | do 24 | curl -L -o $(basename $line).zip --header "Authorization: token ${GITHUB_TOKEN}" --header "Accept: application/octet-stream" --request GET $line 25 | unzip -o $(basename $line).zip -d dist 26 | rm $(basename $line).zip 27 | done 28 | 29 | PLUGIN_BUILD_SLUG=$(jq -r .slug plugin.json) 30 | PLUGIN_BUILD_NAME=${PLUGIN_BUILD_SLUG}-$(jq -r .version plugin.json) 31 | cd dist 32 | zip -q -9 -r ${PLUGIN_BUILD_NAME}.zip ./${PLUGIN_BUILD_SLUG} 33 | cd .. 34 | 35 | ASSET_PATH=$(ls dist/*.zip) 36 | 37 | curl -i \ 38 | --header "Authorization: token ${GITHUB_TOKEN}" \ 39 | --header "Content-Type: application/zip" \ 40 | --request POST \ 41 | --data-binary @"${ASSET_PATH}" \ 42 | ${UPLOAD_URL%"{?name,label\}"}?name=${ASSET_PATH#"dist/"} 43 | -------------------------------------------------------------------------------- /.github/workflows/buildRelease.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [published] 4 | name: Release 5 | env: 6 | RACK_SDK_VERSION: 1.1.6 7 | jobs: 8 | buildLinux: 9 | name: Build Linux 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: Build Linux 14 | uses: ./.github/actions/build_linux 15 | - name: upload zip 16 | run: sh ./.github/actions/upload_zip/script.sh ${{ secrets.GITHUB_TOKEN }} 17 | buildWindows: 18 | name: Build Windows 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@master 22 | - name: Build Windows 23 | uses: ./.github/actions/build_win 24 | - name: upload zip 25 | run: sh ./.github/actions/upload_zip/script.sh ${{ secrets.GITHUB_TOKEN }} 26 | buildOsx: 27 | name: Build OSX 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@master 31 | - name: Build OSX 32 | uses: ./.github/actions/build_osx 33 | - name: upload zip 34 | run: sh ./.github/actions/upload_zip/script.sh ${{ secrets.GITHUB_TOKEN }} 35 | combineDist: 36 | name: Combine Distributions 37 | runs-on: ubuntu-latest 38 | needs: [buildLinux, buildWindows, buildOsx] 39 | steps: 40 | - uses: actions/checkout@master 41 | - name: combine zip 42 | run: sh ./.github/actions/combine_zip/script.sh ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | 2 | ### 1.1.2 (13-09-2020) 3 | - New module: Console MM 4 | - New module: Monitoring 5 | - New module: Golem 6 | - New module: Rasp 7 | - Tape: Fixed unbalanced output when loaded in a patch 8 | 9 | ### 1.1.1 (29-07-2020) 10 | - New module: Holt 11 | - ResEQ: Fixed parameter scaling and glitches 12 | - Tape: Improved performance 13 | - DualBSG: Minimized clicks on parameter changes (monophonic only) 14 | - Hombre: Removed wrong tag 15 | - Some modules: Fixed a bug, where the last voltage would be output after disconnection of input 16 | - All: Revised initialization and reset behaviour 17 | 18 | ### 1.1.0 (05-07-2020) 19 | - New module: ResEQ 20 | - New module: Interstage 21 | - Added/Updated low CPU mode for Chorus, Console, Distance, Hombre, MV, Tape, Tremolo and Vibrato 22 | - All: minor performance optimizations 23 | 24 | ### 1.0.4 (22-05-2020) 25 | - Now polyphonic: Dual BSG, Capacitor, Capacitor ST, Chorus, Console, Distance, Hombre, Tape, Tremolo and Vibrato 26 | - Capacitor, Capacitor ST: added low cpu mode 27 | 28 | ### 1.0.3 (24-04-2020) 29 | 30 | - New module: Tape 31 | - New module: Console 32 | 33 | ### 1.0.2 (13-04-2020) 34 | 35 | - Dual BSG: fixed switch graphic not rendering properly 36 | 37 | ### 1.0.1 (07-04-2020) 38 | 39 | - New module: Capacitor Stereo 40 | - New module: Distance 41 | - New module: Tremolo 42 | 43 | - All: process audio only when an output is connected 44 | - All: minor fixes and code cleanup 45 | 46 | ### 1.0.0 (27-03-2020) 47 | 48 | - Initial release -------------------------------------------------------------------------------- /res/components/rw_switch_three_vert_0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /res/components/rw_switch_three_vert_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /res/components/rw_switch_three_0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /res/components/rw_switch_three_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /res/components/rw_switch_three_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /res/components/rw_switch_three_vert_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### Rackwindows License ### 2 | 3 | MIT License 4 | 5 | Copyright (c) 2020 Jens Robert Janke 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ### Airwindows License ### 26 | 27 | MIT License 28 | 29 | Copyright (c) 2018 Chris Johnson 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in all 39 | copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 47 | SOFTWARE. -------------------------------------------------------------------------------- /.github/actions/build_osx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian 2 | 3 | LABEL "com.github.actions.name"="VCVRackPluginBuilder-OSX" 4 | LABEL "com.github.actions.description"="Builds a VCV Rack plugin for OS X" 5 | LABEL "com.github.actions.icon"="headphones" 6 | LABEL "com.github.actions.color"="purple" 7 | 8 | LABEL "repository"="TBD" 9 | LABEL "homepage"="TBD" 10 | LABEL "maintainer"="n0jo" 11 | 12 | RUN apt-get update && \ 13 | apt-get upgrade -yy && \ 14 | apt-get install -yy \ 15 | automake \ 16 | bison \ 17 | curl \ 18 | file \ 19 | flex \ 20 | git \ 21 | libtool \ 22 | pkg-config \ 23 | python \ 24 | texinfo \ 25 | vim \ 26 | wget \ 27 | zlib1g-dev \ 28 | build-essential \ 29 | cmake \ 30 | make \ 31 | tar \ 32 | unzip \ 33 | zip \ 34 | libgl1-mesa-dev \ 35 | libglu1-mesa-dev \ 36 | jq \ 37 | rsync 38 | 39 | # Install osxcross 40 | # NOTE: The Docker Hub's build machines run varying types of CPUs, so an image 41 | # built with `-march=native` on one of those may not run on every machine - I 42 | # ran into this problem when the images wouldn't run on my 2013-era Macbook 43 | # Pro. As such, we remove this flag entirely. 44 | ENV OSXCROSS_SDK_VERSION 10.11 45 | RUN SDK_VERSION=$OSXCROSS_SDK_VERSION \ 46 | mkdir /opt/osxcross && \ 47 | cd /opt && \ 48 | git clone https://github.com/tpoechtrager/osxcross.git && \ 49 | cd osxcross && \ 50 | git checkout e0a171828a72a0d7ad4409489033536590008ebf && \ 51 | sed -i -e 's|-march=native||g' ./build_clang.sh ./wrapper/build.sh && \ 52 | ./tools/get_dependencies.sh && \ 53 | curl -L -o ./tarballs/MacOSX${OSXCROSS_SDK_VERSION}.sdk.tar.xz \ 54 | https://github.com/apriorit/osxcross-sdks/raw/master/MacOSX${OSXCROSS_SDK_VERSION}.sdk.tar.xz && \ 55 | yes | PORTABLE=true ./build.sh && \ 56 | ./build_compiler_rt.sh 57 | 58 | ENV PATH $PATH:/opt/osxcross/target/bin 59 | 60 | ADD entrypoint.sh /entrypoint.sh 61 | RUN chmod a+x /entrypoint.sh 62 | 63 | ENTRYPOINT ["/entrypoint.sh"] 64 | -------------------------------------------------------------------------------- /res/components/rw_PJ301M.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/plugin.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "components.hpp" 3 | #include 4 | #include 5 | #include "rwlib.h" 6 | 7 | using namespace rack; 8 | 9 | // Declare the Plugin, defined in plugin.cpp 10 | extern Plugin* pluginInstance; 11 | 12 | // Declare each Model, defined in each module source file 13 | // extern Model* modelAcceleration; 14 | extern Model* modelBitshiftgain; 15 | extern Model* modelCapacitor; 16 | extern Model* modelCapacitor_stereo; 17 | extern Model* modelChorus; 18 | extern Model* modelConsole; 19 | extern Model* modelConsole_mm; 20 | extern Model* modelDistance; 21 | extern Model* modelGolem; 22 | extern Model* modelHolt; 23 | extern Model* modelHombre; 24 | extern Model* modelInterstage; 25 | extern Model* modelMonitoring; 26 | extern Model* modelMv; 27 | extern Model* modelRasp; 28 | extern Model* modelReseq; 29 | extern Model* modelTape; 30 | extern Model* modelTremolo; 31 | extern Model* modelVibrato; 32 | 33 | /* Other stuff */ 34 | 35 | /* #quality mode 36 | ======================================================================================== */ 37 | void saveQuality(bool quality); 38 | bool loadQuality(); 39 | void saveHighQualityAsDefault(bool highQualityAsDefault); 40 | bool loadHighQualityAsDefault(); 41 | 42 | struct highQualityDefaultItem : MenuItem { 43 | void onAction(const event::Action& e) override 44 | { 45 | saveHighQualityAsDefault(rightText.empty()); // implicitly toggled 46 | } 47 | }; 48 | 49 | /* #console type (Console, Console MM) 50 | ======================================================================================== */ 51 | void saveConsoleType(int consoleType); 52 | int loadConsoleType(); 53 | 54 | /* #direct out mode (Console MM) 55 | ======================================================================================== */ 56 | void saveDirectOutMode(int directOutMode); 57 | int loadDirectOutMode(); 58 | 59 | /* #slew type (Rasp) 60 | ======================================================================================== */ 61 | void saveSlewType(int slewType); 62 | int loadSlewType(); 63 | 64 | /* #delay mode (Golem) 65 | ======================================================================================== */ 66 | void saveDelayMode(int delayMode); 67 | int loadDelayMode(); 68 | 69 | /* #themes 70 | ======================================================================================== */ 71 | static const std::string lightPanelID = "Light Panel"; 72 | static const std::string darkPanelID = "Dark Panel"; 73 | 74 | // https://github.com/MarcBoule/Geodesics/blob/master/src/Geodesics.hpp 75 | void saveDarkAsDefault(bool darkAsDefault); 76 | bool loadDarkAsDefault(); 77 | 78 | struct DarkDefaultItem : MenuItem { 79 | void onAction(const event::Action& e) override 80 | { 81 | saveDarkAsDefault(rightText.empty()); // implicitly toggled 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /res/components/rw_PJ301M_silver.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /res/components/rw_screw_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 65 | 70 | 75 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /res/components/rw_CKSS_0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 65 | 70 | 73 | 76 | 81 | 82 | 83 | 88 | 93 | 98 | 103 | 108 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /res/components/rw_CKSS_rot_0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 65 | 70 | 73 | 76 | 81 | 82 | 83 | 88 | 93 | 98 | 103 | 108 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /res/components/rw_CKSS_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 65 | 69 | 74 | 75 | 78 | 81 | 86 | 87 | 88 | 93 | 98 | 103 | 108 | 113 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /res/components/rw_CKSS_rot_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 65 | 69 | 74 | 75 | 78 | 81 | 86 | 87 | 88 | 93 | 98 | 103 | 108 | 113 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "slug": "rackwindows", 3 | "name": "Rackwindows", 4 | "version": "1.1.2", 5 | "license": "MIT", 6 | "brand": "Rackwindows", 7 | "author": "Jens Robert Janke", 8 | "authorEmail": "jensrobertjanke@gmail.com", 9 | "authorUrl": "https://github.com/n0jo", 10 | "pluginUrl": "https://github.com/n0jo/rackwindows", 11 | "manualUrl": "https://github.com/n0jo/rackwindows/blob/master/readme.md", 12 | "sourceUrl": "https://github.com/n0jo/rackwindows.git", 13 | "donateUrl": "", 14 | "changelogUrl": "https://github.com/n0jo/rackwindows/blob/master/changelog.md", 15 | "modules": [ 16 | { 17 | "slug": "bitshiftgain", 18 | "name": "Dual BSG", 19 | "description": "Dual gain shifter", 20 | "tags": [ 21 | "Attenuator", 22 | "Dual", 23 | "Polyphonic", 24 | "Utility" 25 | ] 26 | }, 27 | { 28 | "slug": "capacitor", 29 | "name": "Capacitor", 30 | "description": "Filters (Mono)", 31 | "tags": [ 32 | "Equalizer", 33 | "Filter", 34 | "Polyphonic" 35 | ] 36 | }, 37 | { 38 | "slug": "capacitor_stereo", 39 | "name": "Capacitor Stereo", 40 | "description": "Filters (Stereo)", 41 | "tags": [ 42 | "Equalizer", 43 | "Filter", 44 | "Polyphonic" 45 | ] 46 | }, 47 | { 48 | "slug": "chorus", 49 | "name": "Chorus", 50 | "description": "Stereo chorus with multi-tap option", 51 | "tags": [ 52 | "Chorus", 53 | "Delay", 54 | "Effect", 55 | "Polyphonic" 56 | ] 57 | }, 58 | { 59 | "slug": "console", 60 | "name": "Console", 61 | "description": "Stereo summing mixer", 62 | "tags": [ 63 | "Mixer", 64 | "Polyphonic", 65 | "Utility" 66 | ] 67 | }, 68 | { 69 | "slug": "console_mm", 70 | "name": "Console MM", 71 | "description": "Stereo summing mixer to work in conjunction with MindMeld's MixMaster", 72 | "tags": [ 73 | "Mixer", 74 | "Utility" 75 | ] 76 | }, 77 | { 78 | "slug": "distance", 79 | "name": "Distance", 80 | "description": "Far-away-izer", 81 | "tags": [ 82 | "Effect", 83 | "Polyphonic", 84 | "Utility" 85 | ] 86 | }, 87 | { 88 | "slug": "golem", 89 | "name": "Golem", 90 | "description": "Micro-delayable crossfader", 91 | "tags": [ 92 | "Utility" 93 | ] 94 | }, 95 | { 96 | "slug": "holt", 97 | "name": "Holt", 98 | "description": "Resonant lowpass filter focussed on low frequencies", 99 | "tags": [ 100 | "Filter", 101 | "Polyphonic" 102 | ] 103 | }, 104 | { 105 | "slug": "hombre", 106 | "name": "Hombre", 107 | "description": "Texas tone and texture", 108 | "tags": [ 109 | "Effect", 110 | "Polyphonic" 111 | ] 112 | }, 113 | { 114 | "slug": "interstage", 115 | "name": "Interstage", 116 | "description": "Subtle analogifier", 117 | "tags": [ 118 | "Effect", 119 | "Filter", 120 | "Polyphonic" 121 | ] 122 | }, 123 | { 124 | "slug": "monitoring", 125 | "name": "Monitoring", 126 | "description": "Mix checker", 127 | "tags": [ 128 | "Utility" 129 | ] 130 | }, 131 | { 132 | "slug": "mv", 133 | "name": "MV", 134 | "description": "Dual-mono reverb", 135 | "tags": [ 136 | "Effect", 137 | "Reverb" 138 | ] 139 | }, 140 | { 141 | "slug": "rasp", 142 | "name": "Rasp", 143 | "description": "De-Edger, high frequency tamer, acceleration limiter", 144 | "tags": [ 145 | "Equalizer", 146 | "EQ", 147 | "Filter", 148 | "Slew limiter", 149 | "Polyphonic" 150 | ] 151 | }, 152 | { 153 | "slug": "reseq", 154 | "name": "ResEQ", 155 | "description": "Resonance equalizer", 156 | "tags": [ 157 | "Equalizer", 158 | "Filter", 159 | "Polyphonic" 160 | ] 161 | }, 162 | { 163 | "slug": "tape", 164 | "name": "Tape", 165 | "description": "All-purpose tape mojo", 166 | "tags": [ 167 | "Distortion", 168 | "Effect", 169 | "Polyphonic" 170 | ] 171 | }, 172 | { 173 | "slug": "tremolo", 174 | "name": "Tremolo", 175 | "description": "Fluctuating saturation curves", 176 | "tags": [ 177 | "Effect", 178 | "Polyphonic", 179 | "Utility" 180 | ] 181 | }, 182 | { 183 | "slug": "vibrato", 184 | "name": "Vibrato", 185 | "description": "FM vibrator o_O", 186 | "tags": [ 187 | "Chorus", 188 | "Clock generator", 189 | "Effect", 190 | "Flanger", 191 | "Polyphonic", 192 | "Utility" 193 | ] 194 | } 195 | ] 196 | } -------------------------------------------------------------------------------- /src/components.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "rack.hpp" 3 | 4 | using namespace rack; 5 | extern Plugin* pluginInstance; 6 | 7 | // knobs 8 | struct RwKnobLarge : app::SvgKnob { 9 | RwKnobLarge() 10 | { 11 | minAngle = -0.76 * M_PI; 12 | maxAngle = 0.76 * M_PI; 13 | shadow->opacity = 0; 14 | setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_knob_large.svg"))); 15 | } 16 | }; 17 | struct RwKnobLargeDark : app::SvgKnob { 18 | RwKnobLargeDark() 19 | { 20 | minAngle = -0.76 * M_PI; 21 | maxAngle = 0.76 * M_PI; 22 | shadow->opacity = 0.1; 23 | setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_knob_large_dark.svg"))); 24 | } 25 | }; 26 | struct RwKnobMedium : app::SvgKnob { 27 | RwKnobMedium() 28 | { 29 | minAngle = -0.76 * M_PI; 30 | maxAngle = 0.76 * M_PI; 31 | shadow->opacity = 0; 32 | setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_knob_medium.svg"))); 33 | } 34 | }; 35 | struct RwKnobMediumDark : app::SvgKnob { 36 | RwKnobMediumDark() 37 | { 38 | minAngle = -0.76 * M_PI; 39 | maxAngle = 0.76 * M_PI; 40 | shadow->opacity = 0.1; 41 | setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_knob_medium_dark.svg"))); 42 | } 43 | }; 44 | struct RwKnobSmall : app::SvgKnob { 45 | RwKnobSmall() 46 | { 47 | minAngle = -0.76 * M_PI; 48 | maxAngle = 0.76 * M_PI; 49 | shadow->opacity = 0; 50 | setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_knob_small.svg"))); 51 | } 52 | }; 53 | struct RwKnobSmallDark : app::SvgKnob { 54 | RwKnobSmallDark() 55 | { 56 | minAngle = -0.76 * M_PI; 57 | maxAngle = 0.76 * M_PI; 58 | shadow->opacity = 0; 59 | setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_knob_small_dark.svg"))); 60 | } 61 | }; 62 | struct RwKnobTrimpot : app::SvgKnob { 63 | RwKnobTrimpot() 64 | { 65 | minAngle = -0.76 * M_PI; 66 | maxAngle = 0.76 * M_PI; 67 | shadow->opacity = 0.05; 68 | setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_knob_trimpot.svg"))); 69 | } 70 | }; 71 | 72 | // snap knobs 73 | struct RwSwitchKnobLarge : RwKnobLarge { 74 | RwSwitchKnobLarge() 75 | { 76 | snap = true; 77 | } 78 | }; 79 | struct RwSwitchKnobMedium : RwKnobMedium { 80 | RwSwitchKnobMedium() 81 | { 82 | snap = true; 83 | } 84 | }; 85 | struct RwSwitchKnobMediumDark : RwKnobMediumDark { 86 | RwSwitchKnobMediumDark() 87 | { 88 | minAngle = -0.8 * M_PI; 89 | maxAngle = 0.8 * M_PI; 90 | snap = true; 91 | } 92 | }; 93 | struct RwSwitchKnobMediumDarkOneThird : RwKnobMediumDark { 94 | RwSwitchKnobMediumDarkOneThird() 95 | { 96 | minAngle = -0.33 * M_PI; 97 | maxAngle = 0.33 * M_PI; 98 | snap = true; 99 | } 100 | }; 101 | struct RwSwitchKnobMediumDarkTwoThirds : RwKnobMediumDark { 102 | RwSwitchKnobMediumDarkTwoThirds() 103 | { 104 | minAngle = -0.74 * M_PI; 105 | maxAngle = 0.74 * M_PI; 106 | snap = true; 107 | } 108 | }; 109 | struct RwSwitchKnobMediumSix : RwKnobMediumDark { 110 | RwSwitchKnobMediumSix() 111 | { 112 | minAngle = -0.72 * M_PI; 113 | maxAngle = 0.72 * M_PI; 114 | snap = true; 115 | } 116 | }; 117 | struct RwSwitchKnobSmall : RwKnobSmall { 118 | RwSwitchKnobSmall() 119 | { 120 | snap = true; 121 | } 122 | }; 123 | struct RwSwitchKnobSmallDark : RwKnobSmallDark { 124 | RwSwitchKnobSmallDark() 125 | { 126 | snap = true; 127 | } 128 | }; 129 | 130 | // switches 131 | struct RwSwitchThree : SvgSwitch { 132 | RwSwitchThree() 133 | { 134 | addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_switch_three_1.svg"))); 135 | addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_switch_three_0.svg"))); 136 | addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_switch_three_2.svg"))); 137 | } 138 | }; 139 | struct RwSwitchThreeVert : SvgSwitch { 140 | RwSwitchThreeVert() 141 | { 142 | addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_switch_three_vert_1.svg"))); 143 | addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_switch_three_vert_0.svg"))); 144 | addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_switch_three_vert_2.svg"))); 145 | } 146 | }; 147 | struct RwCKSS : SvgSwitch { 148 | RwCKSS() 149 | { 150 | addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_CKSS_0.svg"))); 151 | addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_CKSS_1.svg"))); 152 | } 153 | }; 154 | struct RwCKSSRot : SvgSwitch { 155 | RwCKSSRot() 156 | { 157 | addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_CKSS_rot_0.svg"))); 158 | addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_CKSS_rot_1.svg"))); 159 | } 160 | }; 161 | 162 | // ports 163 | struct RwPJ301MPort : app::SvgPort { 164 | RwPJ301MPort() 165 | { 166 | setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_PJ301M.svg"))); 167 | } 168 | }; 169 | struct RwPJ301MPortSilver : app::SvgPort { 170 | RwPJ301MPortSilver() 171 | { 172 | setSvg(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/rw_PJ301M_silver.svg"))); 173 | } 174 | }; -------------------------------------------------------------------------------- /src/monitoring.cpp: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | Monitoring 3 | ---------- 4 | VCV Rack module based on Monitoring by Chris Johnson from Airwindows 5 | 6 | Ported and designed by Jens Robert Janke 7 | 8 | Changes/Additions: 9 | - separate controls for processing modes and cans 10 | - no monolat, monorat 11 | 12 | See ./LICENSE.md for all licenses 13 | ************************************************************************************************/ 14 | 15 | #include "plugin.hpp" 16 | 17 | using namespace rwlib; 18 | 19 | struct Monitoring : Module { 20 | enum ParamIds { 21 | MODE_PARAM, 22 | CANS_PARAM, 23 | DITHER_PARAM, 24 | NUM_PARAMS 25 | }; 26 | enum InputIds { 27 | IN_L_INPUT, 28 | IN_R_INPUT, 29 | NUM_INPUTS 30 | }; 31 | enum OutputIds { 32 | OUT_L_OUTPUT, 33 | OUT_R_OUTPUT, 34 | NUM_OUTPUTS 35 | }; 36 | enum LightIds { 37 | DITHER_16_LIGHT, 38 | DITHER_24_LIGHT, 39 | NUM_LIGHTS 40 | }; 41 | 42 | // module variables 43 | const double gainFactor = 10.0; 44 | enum processingModes { 45 | OFF, 46 | SUBS, 47 | SLEW, 48 | PEAKS, 49 | MID, 50 | SIDE, 51 | VINYL, 52 | AURAT, 53 | PHONE 54 | }; 55 | enum cansModes { 56 | CANS_OFF, 57 | CANS_A, 58 | CANS_B, 59 | CANS_C, 60 | CANS_D 61 | }; 62 | enum ditherModes { 63 | DITHER_OFF, 64 | DITHER_24, 65 | DITHER_16 66 | }; 67 | 68 | // control parameters 69 | int processingMode; 70 | int lastProcessingMode; 71 | int cansMode; 72 | int ditherMode; 73 | 74 | // state variables 75 | SubsOnly subsL, subsR; 76 | SlewOnly slewL, slewR; 77 | PeaksOnly peaksL, peaksR; 78 | BiquadBandpass bandpassL, bandpassR; 79 | Cans cans; 80 | Dark darkL, darkR; 81 | uint32_t fpd; 82 | 83 | //other 84 | double overallscale; 85 | 86 | Monitoring() 87 | { 88 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 89 | configParam(MODE_PARAM, 0.f, 8.f, 0.f, "Mode"); 90 | configParam(CANS_PARAM, 0.f, 4.f, 0.f, "Cans"); 91 | configParam(DITHER_PARAM, 0.f, 2.f, 0.f, "Dither"); 92 | 93 | onReset(); 94 | } 95 | 96 | void onReset() override 97 | { 98 | onSampleRateChange(); 99 | 100 | subsL = SubsOnly(); 101 | subsR = SubsOnly(); 102 | slewL = SlewOnly(); 103 | slewR = SlewOnly(); 104 | peaksL = PeaksOnly(); 105 | peaksR = PeaksOnly(); 106 | bandpassL = BiquadBandpass(); 107 | bandpassR = BiquadBandpass(); 108 | cans = Cans(); 109 | darkL = Dark(); 110 | darkR = Dark(); 111 | 112 | processingMode = 0; 113 | cansMode = 0; 114 | lastProcessingMode = 0; 115 | ditherMode = 0; 116 | 117 | fpd = 17; 118 | } 119 | 120 | void onSampleRateChange() override 121 | { 122 | float sampleRate = APP->engine->getSampleRate(); 123 | 124 | overallscale = 1.0; 125 | overallscale /= 44100.0; 126 | overallscale *= sampleRate; 127 | } 128 | 129 | void process(const ProcessArgs& args) override 130 | { 131 | // dither light 132 | ditherMode = params[DITHER_PARAM].getValue(); 133 | lights[DITHER_24_LIGHT].setBrightness(ditherMode == DITHER_24 ? 1.f : 0.f); 134 | lights[DITHER_16_LIGHT].setBrightness(ditherMode == DITHER_16 ? 1.f : 0.f); 135 | 136 | if (outputs[OUT_L_OUTPUT].isConnected() || outputs[OUT_R_OUTPUT].isConnected()) { 137 | 138 | // get params 139 | processingMode = params[MODE_PARAM].getValue(); 140 | cansMode = params[CANS_PARAM].getValue(); 141 | 142 | if (processingMode != lastProcessingMode) { 143 | // set up bandpass for vinyl, aurat and phone 144 | if (processingMode == VINYL) { 145 | bandpassL.set(0.0385 / overallscale, 0.0825); 146 | bandpassR.set(0.0385 / overallscale, 0.0825); 147 | } 148 | if (processingMode == AURAT) { 149 | bandpassL.set(0.0375 / overallscale, 0.1575); 150 | bandpassR.set(0.0375 / overallscale, 0.1575); 151 | } 152 | if (processingMode == PHONE) { 153 | bandpassL.set(0.1245 / overallscale, 0.46); 154 | bandpassR.set(0.1245 / overallscale, 0.46); 155 | } 156 | lastProcessingMode = processingMode; 157 | } 158 | 159 | // get input 160 | long double inputSampleL = inputs[IN_L_INPUT].getVoltage(); 161 | long double inputSampleR = inputs[IN_R_INPUT].getVoltage(); 162 | 163 | // pad gain 164 | inputSampleL /= gainFactor; 165 | inputSampleR /= gainFactor; 166 | 167 | // prepare mid and side 168 | long double mid = inputSampleL + inputSampleR; 169 | long double side = inputSampleL - inputSampleR; 170 | 171 | // processing modes 172 | switch (processingMode) { 173 | case OFF: 174 | break; 175 | 176 | case SUBS: 177 | inputSampleL = subsL.process(inputSampleL, overallscale); 178 | inputSampleR = subsR.process(inputSampleR, overallscale); 179 | break; 180 | 181 | case SLEW: 182 | inputSampleL = slewL.process(inputSampleL); 183 | inputSampleR = slewR.process(inputSampleR); 184 | break; 185 | 186 | case PEAKS: 187 | inputSampleL = peaksL.process(inputSampleL, overallscale); 188 | inputSampleR = peaksR.process(inputSampleR, overallscale); 189 | break; 190 | 191 | case MID: 192 | inputSampleL = mid * 0.5; 193 | inputSampleR = mid * 0.5; 194 | break; 195 | 196 | case SIDE: 197 | inputSampleL = side * 0.5; 198 | inputSampleR = -side * 0.5; 199 | break; 200 | 201 | case VINYL: 202 | inputSampleL = bandpassL.process(inputSampleL); 203 | inputSampleR = bandpassR.process(inputSampleR); 204 | break; 205 | 206 | case AURAT: 207 | inputSampleL = bandpassL.process(inputSampleL); 208 | inputSampleR = bandpassR.process(inputSampleR); 209 | break; 210 | 211 | case PHONE: 212 | inputSampleL = bandpassL.process(mid * 0.5); 213 | inputSampleR = bandpassR.process(mid * 0.5); 214 | break; 215 | } 216 | 217 | // cans 218 | if (cansMode) { 219 | cans.setMode(cansMode); 220 | cans.process(inputSampleL, inputSampleR, overallscale); 221 | } 222 | 223 | // dither 224 | switch (ditherMode) { 225 | case DITHER_OFF: 226 | break; 227 | case DITHER_16: 228 | inputSampleL = darkL.process(inputSampleL, overallscale, false); 229 | inputSampleR = darkR.process(inputSampleR, overallscale, false); 230 | break; 231 | case DITHER_24: 232 | inputSampleL = darkL.process(inputSampleL, overallscale, true); 233 | inputSampleR = darkR.process(inputSampleR, overallscale, true); 234 | break; 235 | } 236 | 237 | // bring gain back up 238 | inputSampleL *= gainFactor; 239 | inputSampleR *= gainFactor; 240 | 241 | // output 242 | outputs[OUT_L_OUTPUT].setVoltage(inputSampleL); 243 | outputs[OUT_R_OUTPUT].setVoltage(inputSampleR); 244 | } 245 | } 246 | }; 247 | 248 | struct MonitoringWidget : ModuleWidget { 249 | 250 | MonitoringWidget(Monitoring* module) 251 | { 252 | setModule(module); 253 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/monitoring_dark.svg"))); 254 | 255 | //screws 256 | addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); 257 | addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); 258 | addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 259 | addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 260 | 261 | // knobs 262 | addParam(createParamCentered(Vec(52.5, 85.0), module, Monitoring::MODE_PARAM)); 263 | addParam(createParamCentered(Vec(52.5, 165.0), module, Monitoring::CANS_PARAM)); 264 | 265 | // switch 266 | addParam(createParamCentered(Vec(52.5, 235.0), module, Monitoring::DITHER_PARAM)); 267 | 268 | // lights 269 | addChild(createLightCentered>(Vec(18.8, 235.0), module, Monitoring::DITHER_24_LIGHT)); 270 | addChild(createLightCentered>(Vec(86.3, 235.0), module, Monitoring::DITHER_16_LIGHT)); 271 | 272 | // inputs 273 | addInput(createInputCentered(Vec(33.75, 285.0), module, Monitoring::IN_L_INPUT)); 274 | addInput(createInputCentered(Vec(71.25, 285.0), module, Monitoring::IN_R_INPUT)); 275 | 276 | // outputs 277 | addOutput(createOutputCentered(Vec(33.75, 325.0), module, Monitoring::OUT_L_OUTPUT)); 278 | addOutput(createOutputCentered(Vec(71.25, 325.0), module, Monitoring::OUT_R_OUTPUT)); 279 | } 280 | }; 281 | 282 | Model* modelMonitoring = createModel("monitoring"); -------------------------------------------------------------------------------- /src/tape.cpp: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | Tape 3 | ---- 4 | VCV Rack module based on Tape by Chris Johnson from Airwindows 5 | 6 | Ported and designed by Jens Robert Janke 7 | 8 | Changes/Additions: 9 | - cv inputs for slam and bump 10 | - polyphonic 11 | 12 | See ./LICENSE.md for all licenses 13 | ************************************************************************************************/ 14 | 15 | #include "plugin.hpp" 16 | 17 | // quality options 18 | #define ECO 0 19 | #define HIGH 1 20 | 21 | // polyphony 22 | #define MAX_POLY_CHANNELS 16 23 | 24 | struct Tape : Module { 25 | enum ParamIds { 26 | SLAM_PARAM, 27 | BUMP_PARAM, 28 | NUM_PARAMS 29 | }; 30 | enum InputIds { 31 | SLAM_CV_INPUT, 32 | BUMP_CV_INPUT, 33 | IN_L_INPUT, 34 | IN_R_INPUT, 35 | NUM_INPUTS 36 | }; 37 | enum OutputIds { 38 | OUT_L_OUTPUT, 39 | OUT_R_OUTPUT, 40 | NUM_OUTPUTS 41 | }; 42 | enum LightIds { 43 | NUM_LIGHTS 44 | }; 45 | 46 | // module variables 47 | const double gainCut = 0.1; 48 | const double gainBoost = 10.0; 49 | int quality; 50 | 51 | // control parameters 52 | float slamParam; 53 | float bumpParam; 54 | 55 | // state variables (as arrays in order to handle up to 16 polyphonic channels) 56 | rwlib::Tape tapeL[MAX_POLY_CHANNELS]; 57 | rwlib::Tape tapeR[MAX_POLY_CHANNELS]; 58 | uint32_t fpdL[MAX_POLY_CHANNELS]; 59 | uint32_t fpdR[MAX_POLY_CHANNELS]; 60 | 61 | // other 62 | double overallscale; 63 | 64 | Tape() 65 | { 66 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 67 | configParam(SLAM_PARAM, 0.f, 1.f, 0.5f, "Slam", "%", 0, 100); 68 | configParam(BUMP_PARAM, 0.f, 1.f, 0.5f, "Bump", "%", 0, 100); 69 | 70 | quality = loadQuality(); 71 | onReset(); 72 | } 73 | 74 | void onReset() override 75 | { 76 | onSampleRateChange(); 77 | 78 | for (int i = 0; i < MAX_POLY_CHANNELS; i++) { 79 | tapeL[i] = rwlib::Tape(); 80 | tapeR[i] = rwlib::Tape(); 81 | 82 | fpdL[i] = fpdR[i] = 17; 83 | } 84 | } 85 | 86 | void onSampleRateChange() override 87 | { 88 | float sampleRate = APP->engine->getSampleRate(); 89 | 90 | overallscale = 1.0; 91 | overallscale /= 44100.0; 92 | overallscale *= sampleRate; 93 | 94 | for (int i = 0; i < MAX_POLY_CHANNELS; i++) { 95 | tapeL[i].onSampleRateChange(overallscale); 96 | tapeR[i].onSampleRateChange(overallscale); 97 | } 98 | } 99 | 100 | json_t* dataToJson() override 101 | { 102 | json_t* rootJ = json_object(); 103 | 104 | // quality 105 | json_object_set_new(rootJ, "quality", json_integer(quality)); 106 | 107 | return rootJ; 108 | } 109 | 110 | void dataFromJson(json_t* rootJ) override 111 | { 112 | // quality 113 | json_t* qualityJ = json_object_get(rootJ, "quality"); 114 | if (qualityJ) 115 | quality = json_integer_value(qualityJ); 116 | } 117 | 118 | void process(const ProcessArgs& args) override 119 | { 120 | slamParam = params[SLAM_PARAM].getValue(); 121 | slamParam += inputs[SLAM_CV_INPUT].getVoltage() / 10; 122 | slamParam = clamp(slamParam, 0.01f, 0.99f); 123 | 124 | bumpParam = params[BUMP_PARAM].getValue(); 125 | bumpParam += inputs[BUMP_CV_INPUT].getVoltage() / 10; 126 | bumpParam = clamp(bumpParam, 0.01f, 0.99f); 127 | 128 | // number of polyphonic channels 129 | int numChannelsL = std::max(1, inputs[IN_L_INPUT].getChannels()); 130 | int numChannelsR = std::max(1, inputs[IN_R_INPUT].getChannels()); 131 | 132 | // process left channel 133 | if (outputs[OUT_L_OUTPUT].isConnected()) { 134 | 135 | // for each poly channel left 136 | for (int i = 0; i < numChannelsL; i++) { 137 | 138 | // input 139 | long double inputSampleL = inputs[IN_L_INPUT].getPolyVoltage(i); 140 | 141 | // pad gain 142 | inputSampleL *= gainCut; 143 | 144 | if (quality == HIGH) { 145 | if (fabs(inputSampleL) < 1.18e-37) 146 | inputSampleL = fpdL[i] * 1.18e-37; 147 | } 148 | 149 | // work the magic 150 | inputSampleL = tapeL[i].process(inputSampleL, slamParam, bumpParam, overallscale); 151 | 152 | if (quality == HIGH) { 153 | //32 bit stereo floating point dither 154 | int expon; 155 | frexpf((float)inputSampleL, &expon); 156 | fpdL[i] ^= fpdL[i] << 13; 157 | fpdL[i] ^= fpdL[i] >> 17; 158 | fpdL[i] ^= fpdL[i] << 5; 159 | inputSampleL += ((double(fpdL[i]) - uint32_t(0x7fffffff)) * 5.5e-36l * pow(2, expon + 62)); 160 | } 161 | 162 | // bring gain back up 163 | inputSampleL *= gainBoost; 164 | 165 | // output 166 | outputs[OUT_L_OUTPUT].setChannels(numChannelsL); 167 | outputs[OUT_L_OUTPUT].setVoltage(inputSampleL, i); 168 | } 169 | } 170 | 171 | // process right channel 172 | if (outputs[OUT_R_OUTPUT].isConnected()) { 173 | 174 | // for each poly channel right 175 | for (int i = 0; i < numChannelsR; i++) { 176 | 177 | // input 178 | long double inputSampleR = inputs[IN_R_INPUT].getPolyVoltage(i); 179 | 180 | // pad gain 181 | inputSampleR *= gainCut; 182 | 183 | if (quality == HIGH) { 184 | if (fabs(inputSampleR) < 1.18e-37) 185 | inputSampleR = fpdR[i] * 1.18e-37; 186 | } 187 | 188 | // work the magic 189 | inputSampleR = tapeR[i].process(inputSampleR, slamParam, bumpParam, overallscale); 190 | 191 | if (quality == HIGH) { 192 | //32 bit stereo floating point dither 193 | int expon; 194 | frexpf((float)inputSampleR, &expon); 195 | fpdR[i] ^= fpdR[i] << 13; 196 | fpdR[i] ^= fpdR[i] >> 17; 197 | fpdR[i] ^= fpdR[i] << 5; 198 | inputSampleR += ((double(fpdR[i]) - uint32_t(0x7fffffff)) * 5.5e-36l * pow(2, expon + 62)); 199 | } 200 | 201 | // bring gain back up 202 | inputSampleR *= gainBoost; 203 | 204 | // output 205 | outputs[OUT_R_OUTPUT].setChannels(numChannelsR); 206 | outputs[OUT_R_OUTPUT].setVoltage(inputSampleR, i); 207 | } 208 | } 209 | } 210 | }; 211 | 212 | struct TapeWidget : ModuleWidget { 213 | 214 | // quality item 215 | struct QualityItem : MenuItem { 216 | Tape* module; 217 | int quality; 218 | 219 | void onAction(const event::Action& e) override 220 | { 221 | module->quality = quality; 222 | } 223 | 224 | void step() override 225 | { 226 | rightText = (module->quality == quality) ? "✔" : ""; 227 | } 228 | }; 229 | 230 | void appendContextMenu(Menu* menu) override 231 | { 232 | Tape* module = dynamic_cast(this->module); 233 | assert(module); 234 | 235 | menu->addChild(new MenuSeparator()); // separator 236 | 237 | MenuLabel* qualityLabel = new MenuLabel(); // menu label 238 | qualityLabel->text = "Quality"; 239 | menu->addChild(qualityLabel); 240 | 241 | QualityItem* low = new QualityItem(); // low quality 242 | low->text = "Eco"; 243 | low->module = module; 244 | low->quality = 0; 245 | menu->addChild(low); 246 | 247 | QualityItem* high = new QualityItem(); // high quality 248 | high->text = "High"; 249 | high->module = module; 250 | high->quality = 1; 251 | menu->addChild(high); 252 | } 253 | 254 | TapeWidget(Tape* module) 255 | { 256 | setModule(module); 257 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/tape_dark.svg"))); 258 | 259 | addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); 260 | addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); 261 | addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 262 | addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 263 | 264 | // knobs 265 | addParam(createParamCentered(Vec(45.0, 75.0), module, Tape::SLAM_PARAM)); 266 | addParam(createParamCentered(Vec(45.0, 145.0), module, Tape::BUMP_PARAM)); 267 | 268 | // inputs 269 | addInput(createInputCentered(Vec(26.25, 245.0), module, Tape::SLAM_CV_INPUT)); 270 | addInput(createInputCentered(Vec(63.75, 245.0), module, Tape::BUMP_CV_INPUT)); 271 | addInput(createInputCentered(Vec(26.25, 285.0), module, Tape::IN_L_INPUT)); 272 | addInput(createInputCentered(Vec(63.75, 285.0), module, Tape::IN_R_INPUT)); 273 | 274 | // outputs 275 | addOutput(createOutputCentered(Vec(26.25, 325.0), module, Tape::OUT_L_OUTPUT)); 276 | addOutput(createOutputCentered(Vec(63.75, 325.0), module, Tape::OUT_R_OUTPUT)); 277 | } 278 | }; 279 | 280 | Model* modelTape = createModel("tape"); -------------------------------------------------------------------------------- /src/interstage.cpp: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | Interstage 3 | -------- 4 | VCV Rack module based on Interstage by Chris Johnson from Airwindows 5 | 6 | Ported and designed by Jens Robert Janke 7 | 8 | Changes/Additions: 9 | - polyphonic 10 | 11 | See ./LICENSE.md for all licenses 12 | ************************************************************************************************/ 13 | 14 | #include "plugin.hpp" 15 | 16 | // quality options 17 | #define ECO 0 18 | #define HIGH 1 19 | 20 | struct Interstage : Module { 21 | enum ParamIds { 22 | NUM_PARAMS 23 | }; 24 | enum InputIds { 25 | IN_L_INPUT, 26 | IN_R_INPUT, 27 | NUM_INPUTS 28 | }; 29 | enum OutputIds { 30 | OUT_L_OUTPUT, 31 | OUT_R_OUTPUT, 32 | NUM_OUTPUTS 33 | }; 34 | enum LightIds { 35 | NUM_LIGHTS 36 | }; 37 | 38 | // module variables 39 | const double gainCut = 0.03125; 40 | const double gainBoost = 32.0; 41 | int quality; 42 | 43 | // state variables (as arrays in order to handle up to 16 polyphonic channels) 44 | double iirSampleAL[16]; 45 | double iirSampleBL[16]; 46 | double iirSampleCL[16]; 47 | double iirSampleDL[16]; 48 | double iirSampleEL[16]; 49 | double iirSampleFL[16]; 50 | long double lastSampleL[16]; 51 | bool flipL[16]; 52 | uint32_t fpdL[16]; 53 | double iirSampleAR[16]; 54 | double iirSampleBR[16]; 55 | double iirSampleCR[16]; 56 | double iirSampleDR[16]; 57 | double iirSampleER[16]; 58 | double iirSampleFR[16]; 59 | long double lastSampleR[16]; 60 | bool flipR[16]; 61 | uint32_t fpdR[16]; 62 | 63 | // other variables, which do not need to be updated every cycle 64 | double overallscale; 65 | double firstStage; 66 | double iirAmount; 67 | 68 | // constants 69 | const double threshold = 0.381966011250105; 70 | 71 | Interstage() 72 | { 73 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 74 | 75 | quality = loadQuality(); 76 | onReset(); 77 | } 78 | 79 | void onReset() override 80 | { 81 | onSampleRateChange(); 82 | 83 | for (int i = 0; i < 16; i++) { 84 | iirSampleAL[i] = iirSampleBL[i] = iirSampleCL[i] = iirSampleDL[i] = iirSampleEL[i] = iirSampleFL[i] = lastSampleL[i] = 0.0; 85 | flipL[i] = true; 86 | fpdR[i] = 17; 87 | iirSampleAR[i] = iirSampleBR[i] = iirSampleCR[i] = iirSampleDR[i] = iirSampleER[i] = iirSampleFR[i] = lastSampleR[i] = 0.0; 88 | flipR[i] = true; 89 | fpdR[i] = 17; 90 | } 91 | } 92 | 93 | void onSampleRateChange() override 94 | { 95 | float sampleRate = APP->engine->getSampleRate(); 96 | 97 | overallscale = 1.0; 98 | overallscale /= 44100.0; 99 | overallscale *= sampleRate; 100 | 101 | firstStage = 0.381966011250105 / overallscale; 102 | iirAmount = 0.00295 / overallscale; 103 | } 104 | 105 | json_t* dataToJson() override 106 | { 107 | json_t* rootJ = json_object(); 108 | 109 | // quality 110 | json_object_set_new(rootJ, "quality", json_integer(quality)); 111 | 112 | return rootJ; 113 | } 114 | 115 | void dataFromJson(json_t* rootJ) override 116 | { 117 | // quality 118 | json_t* qualityJ = json_object_get(rootJ, "quality"); 119 | if (qualityJ) 120 | quality = json_integer_value(qualityJ); 121 | } 122 | 123 | void processChannel(Input& input, Output& output, double iirSampleA[], double iirSampleB[], double iirSampleC[], double iirSampleD[], double iirSampleE[], double iirSampleF[], long double lastSample[], bool flip[], uint32_t fpd[]) 124 | { 125 | if (output.isConnected()) { 126 | 127 | long double inputSample; 128 | long double drySample; 129 | 130 | // number of polyphonic channels 131 | int numChannels = std::max(1, input.getChannels()); 132 | 133 | // for each poly channel 134 | for (int i = 0; i < numChannels; i++) { 135 | 136 | // input 137 | inputSample = input.getPolyVoltage(i); 138 | 139 | // pad gain 140 | inputSample *= gainCut; 141 | 142 | if (quality == HIGH) { 143 | if (fabs(inputSample) < 1.18e-37) 144 | inputSample = fpd[i] * 1.18e-37; 145 | } 146 | 147 | drySample = inputSample; 148 | 149 | inputSample = (inputSample + lastSample[i]) * 0.5; //start the lowpassing with an average 150 | 151 | if (flip[i]) { 152 | //make highpass 153 | iirSampleA[i] = (iirSampleA[i] * (1 - firstStage)) + (inputSample * firstStage); 154 | inputSample = iirSampleA[i]; 155 | iirSampleC[i] = (iirSampleC[i] * (1 - iirAmount)) + (inputSample * iirAmount); 156 | inputSample = iirSampleC[i]; 157 | iirSampleE[i] = (iirSampleE[i] * (1 - iirAmount)) + (inputSample * iirAmount); 158 | inputSample = iirSampleE[i]; 159 | inputSample = drySample - inputSample; 160 | 161 | //slew limit against lowpassed reference point 162 | if (inputSample - iirSampleA[i] > threshold) 163 | inputSample = iirSampleA[i] + threshold; 164 | if (inputSample - iirSampleA[i] < -threshold) 165 | inputSample = iirSampleA[i] - threshold; 166 | 167 | } else { 168 | //make highpass 169 | iirSampleB[i] = (iirSampleB[i] * (1 - firstStage)) + (inputSample * firstStage); 170 | inputSample = iirSampleB[i]; 171 | iirSampleD[i] = (iirSampleD[i] * (1 - iirAmount)) + (inputSample * iirAmount); 172 | inputSample = iirSampleD[i]; 173 | iirSampleF[i] = (iirSampleF[i] * (1 - iirAmount)) + (inputSample * iirAmount); 174 | inputSample = iirSampleF[i]; 175 | inputSample = drySample - inputSample; 176 | 177 | //slew limit against lowpassed reference point 178 | if (inputSample - iirSampleB[i] > threshold) 179 | inputSample = iirSampleB[i] + threshold; 180 | if (inputSample - iirSampleB[i] < -threshold) 181 | inputSample = iirSampleB[i] - threshold; 182 | } 183 | 184 | flip[i] = !flip[i]; 185 | 186 | lastSample[i] = inputSample; 187 | 188 | if (quality == HIGH) { 189 | //begin 32 bit stereo floating point dither 190 | int expon; 191 | frexpf((float)inputSample, &expon); 192 | fpd[i] ^= fpd[i] << 13; 193 | fpd[i] ^= fpd[i] >> 17; 194 | fpd[i] ^= fpd[i] << 5; 195 | inputSample += ((double(fpd[i]) - uint32_t(0x7fffffff)) * 5.5e-36l * pow(2, expon + 62)); 196 | //end 32 bit stereo floating point dither 197 | } 198 | 199 | // bring gain back up 200 | inputSample *= gainBoost; 201 | 202 | // output 203 | output.setChannels(numChannels); 204 | output.setVoltage(inputSample, i); 205 | } 206 | } 207 | } 208 | 209 | void process(const ProcessArgs& args) override 210 | { 211 | processChannel(inputs[IN_L_INPUT], outputs[OUT_L_OUTPUT], iirSampleAL, iirSampleBL, iirSampleCL, iirSampleDL, iirSampleEL, iirSampleFL, lastSampleL, flipL, fpdL); 212 | processChannel(inputs[IN_R_INPUT], outputs[OUT_R_OUTPUT], iirSampleAR, iirSampleBR, iirSampleCR, iirSampleDR, iirSampleER, iirSampleFR, lastSampleR, flipR, fpdR); 213 | } 214 | }; 215 | 216 | struct InterstageWidget : ModuleWidget { 217 | 218 | // quality item 219 | struct QualityItem : MenuItem { 220 | Interstage* module; 221 | int quality; 222 | 223 | void onAction(const event::Action& e) override 224 | { 225 | module->quality = quality; 226 | } 227 | 228 | void step() override 229 | { 230 | rightText = (module->quality == quality) ? "✔" : ""; 231 | } 232 | }; 233 | 234 | void appendContextMenu(Menu* menu) override 235 | { 236 | Interstage* module = dynamic_cast(this->module); 237 | assert(module); 238 | 239 | menu->addChild(new MenuSeparator()); // separator 240 | 241 | MenuLabel* qualityLabel = new MenuLabel(); // menu label 242 | qualityLabel->text = "Quality"; 243 | menu->addChild(qualityLabel); 244 | 245 | QualityItem* low = new QualityItem(); // low quality 246 | low->text = "Eco"; 247 | low->module = module; 248 | low->quality = 0; 249 | menu->addChild(low); 250 | 251 | QualityItem* high = new QualityItem(); // high quality 252 | high->text = "High"; 253 | high->module = module; 254 | high->quality = 1; 255 | menu->addChild(high); 256 | } 257 | 258 | InterstageWidget(Interstage* module) 259 | { 260 | setModule(module); 261 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/interstage_dark.svg"))); 262 | 263 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 1.5, 0))); 264 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 1.5, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 265 | 266 | addInput(createInputCentered(Vec(30.0, 205.0), module, Interstage::IN_L_INPUT)); 267 | addInput(createInputCentered(Vec(30.0, 245.0), module, Interstage::IN_R_INPUT)); 268 | 269 | addOutput(createOutputCentered(Vec(30.0, 285.0), module, Interstage::OUT_L_OUTPUT)); 270 | addOutput(createOutputCentered(Vec(30.0, 325.0), module, Interstage::OUT_R_OUTPUT)); 271 | } 272 | }; 273 | 274 | Model* modelInterstage = createModel("interstage"); -------------------------------------------------------------------------------- /src/bitshiftgain.cpp: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | Dual BSG 3 | -------- 4 | VCV Rack module based on BitshiftGain by Chris Johnson from Airwindows 5 | 6 | Ported and designed by Jens Robert Janke 7 | 8 | Changes/Additions: 9 | - 2 BSG units 10 | - if no input is connected, the respective output will provide constant voltage selectable in 1V steps from -8V to +8V 11 | - option to link bottom BSG to top BSG -> gain shifts at top BSG are automatically compensated for by the bottom BSG 12 | - if linked, bottom knob acts as an offset 13 | - polyphonic 14 | 15 | See ./LICENSE.md for all licenses 16 | ************************************************************************************************/ 17 | 18 | // CAUTION: the output is not limited in any way, positive values can produce very high volumes 19 | 20 | #include "plugin.hpp" 21 | 22 | struct Bitshiftgain : Module { 23 | enum ParamIds { 24 | SHIFT_A_PARAM, 25 | SHIFT_B_PARAM, 26 | LINK_PARAM, 27 | NUM_PARAMS 28 | }; 29 | enum InputIds { 30 | IN_A_INPUT, 31 | IN_B_INPUT, 32 | NUM_INPUTS 33 | }; 34 | enum OutputIds { 35 | OUT_A_OUTPUT, 36 | OUT_B_OUTPUT, 37 | NUM_OUTPUTS 38 | }; 39 | enum LightIds { 40 | LINK_LIGHT, 41 | NUM_LIGHTS 42 | }; 43 | 44 | int shiftA; 45 | int shiftB; 46 | bool isLinked; 47 | double lastSampleA; // for zero crossing detection 48 | double lastSampleB; // for zero crossing detection 49 | 50 | Bitshiftgain() 51 | { 52 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 53 | configParam(SHIFT_A_PARAM, -8.0, 8.0, 0.0, "Shift"); 54 | configParam(SHIFT_B_PARAM, -8.0, 8.0, 0.0, "Shift/Offset"); 55 | configParam(LINK_PARAM, 0.f, 1.f, 0.0, "Link"); 56 | 57 | onReset(); 58 | } 59 | 60 | void onReset() override 61 | { 62 | shiftA = 0; 63 | shiftB = 0; 64 | isLinked = false; 65 | lastSampleA = 0.0; 66 | lastSampleB = 0.0; 67 | } 68 | 69 | double bitShift(int bitshiftGain) 70 | { 71 | double gain = 1.0; 72 | 73 | //we are directly punching in the gain values rather than calculating them 74 | switch (bitshiftGain) { 75 | case -16: 76 | gain = 0.0000152587890625; 77 | break; 78 | case -15: 79 | gain = 0.000030517578125; 80 | break; 81 | case -14: 82 | gain = 0.00006103515625; 83 | break; 84 | case -13: 85 | gain = 0.0001220703125; 86 | break; 87 | case -12: 88 | gain = 0.000244140625; 89 | break; 90 | case -11: 91 | gain = 0.00048828125; 92 | break; 93 | case -10: 94 | gain = 0.0009765625; 95 | break; 96 | case -9: 97 | gain = 0.001953125; 98 | break; 99 | case -8: 100 | gain = 0.00390625; 101 | break; 102 | case -7: 103 | gain = 0.0078125; 104 | break; 105 | case -6: 106 | gain = 0.015625; 107 | break; 108 | case -5: 109 | gain = 0.03125; 110 | break; 111 | case -4: 112 | gain = 0.0625; 113 | break; 114 | case -3: 115 | gain = 0.125; 116 | break; 117 | case -2: 118 | gain = 0.25; 119 | break; 120 | case -1: 121 | gain = 0.5; 122 | break; 123 | case 0: 124 | gain = 1.0; 125 | break; 126 | case 1: 127 | gain = 2.0; 128 | break; 129 | case 2: 130 | gain = 4.0; 131 | break; 132 | case 3: 133 | gain = 8.0; 134 | break; 135 | case 4: 136 | gain = 16.0; 137 | break; 138 | case 5: 139 | gain = 32.0; 140 | break; 141 | case 6: 142 | gain = 64.0; 143 | break; 144 | case 7: 145 | gain = 128.0; 146 | break; 147 | case 8: 148 | gain = 256.0; 149 | break; 150 | case 9: 151 | gain = 512.0; 152 | break; 153 | case 10: 154 | gain = 1024.0; 155 | break; 156 | case 11: 157 | gain = 2048.0; 158 | break; 159 | case 12: 160 | gain = 4096.0; 161 | break; 162 | case 13: 163 | gain = 8192.0; 164 | break; 165 | case 14: 166 | gain = 16384.0; 167 | break; 168 | case 15: 169 | gain = 32768.0; 170 | break; 171 | case 16: 172 | gain = 65536.0; 173 | break; 174 | } 175 | 176 | return gain; 177 | } 178 | 179 | void process(const ProcessArgs& args) override 180 | { 181 | // link 182 | isLinked = params[LINK_PARAM].getValue() ? true : false; 183 | lights[LINK_LIGHT].setBrightness(isLinked); 184 | 185 | /* section A 186 | =============================================================================== */ 187 | 188 | if (inputs[IN_A_INPUT].isConnected()) { 189 | 190 | // get number of polyphonic channels 191 | int numChannelsA = inputs[IN_A_INPUT].getChannels(); 192 | 193 | // set number of output channels 194 | outputs[OUT_A_OUTPUT].setChannels(numChannelsA); 195 | 196 | // update shiftA only at zero crossings (of first channel) to reduce clicks on parameter changes 197 | // reasonably effective on most sources, but will not happen across multiple channels, therefore monophonic only 198 | bool isZero = (inputs[IN_A_INPUT].getVoltage() * lastSampleA < 0.0); 199 | shiftA = isZero ? params[SHIFT_A_PARAM].getValue() : shiftA; 200 | lastSampleA = inputs[IN_A_INPUT].getVoltage(); 201 | 202 | // for each poly channel 203 | for (int i = 0; i < numChannelsA; i++) { 204 | // shift signal in 6db steps 205 | outputs[OUT_A_OUTPUT].setVoltage(inputs[IN_A_INPUT].getPolyVoltage(i) * bitShift(shiftA), i); 206 | } 207 | } else { 208 | // output -8 to 8 in 1V steps if no input is connected 209 | outputs[OUT_A_OUTPUT].setVoltage(params[SHIFT_A_PARAM].getValue()); 210 | } 211 | 212 | /* section B 213 | =============================================================================== */ 214 | 215 | if (inputs[IN_B_INPUT].isConnected()) { 216 | 217 | // get number of polyphonic channels 218 | int numChannelsB = inputs[IN_B_INPUT].getChannels(); 219 | 220 | // set number of output channels 221 | outputs[OUT_B_OUTPUT].setChannels(numChannelsB); 222 | 223 | // update shiftB only at zero crossings (of first channel) to reduce clicks on parameter changes 224 | // reasonably effective on most sources, but will not happen across multiple channels, therefore monophonic only 225 | bool isZero = (inputs[IN_B_INPUT].getVoltage() * lastSampleB < 0.0); 226 | shiftB = isZero ? params[SHIFT_B_PARAM].getValue() : shiftB; 227 | lastSampleB = inputs[IN_B_INPUT].getVoltage(); 228 | 229 | // for each poly channel 230 | for (int i = 0; i < numChannelsB; i++) { 231 | if (isLinked) { 232 | if (inputs[IN_A_INPUT].isConnected()) { 233 | // offset signal in 6db steps 234 | outputs[OUT_B_OUTPUT].setVoltage(inputs[IN_B_INPUT].getPolyVoltage(i) * bitShift(-shiftA + shiftB), i); 235 | } else { 236 | // offset signal in 1V steps 237 | outputs[OUT_B_OUTPUT].setVoltage(inputs[IN_B_INPUT].getPolyVoltage(i) + params[SHIFT_B_PARAM].getValue(), i); 238 | } 239 | } else { 240 | // shift signal in 6db steps 241 | outputs[OUT_B_OUTPUT].setVoltage(inputs[IN_B_INPUT].getPolyVoltage(i) * bitShift(shiftB), i); 242 | } 243 | } 244 | } else { 245 | // output -8 to 8 in 1V steps if no input is connected 246 | outputs[OUT_B_OUTPUT].setVoltage(params[SHIFT_B_PARAM].getValue()); 247 | } 248 | } 249 | }; 250 | 251 | struct BitshiftgainWidget : ModuleWidget { 252 | BitshiftgainWidget(Bitshiftgain* module) 253 | { 254 | setModule(module); 255 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/bitshiftgain_dark.svg"))); 256 | 257 | // screws 258 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 1.5, 0))); 259 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 1.5, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 260 | 261 | // knobs 262 | addParam(createParamCentered(Vec(30.0, 65.0), module, Bitshiftgain::SHIFT_A_PARAM)); 263 | addParam(createParamCentered(Vec(30.0, 235.0), module, Bitshiftgain::SHIFT_B_PARAM)); 264 | 265 | // switches 266 | addParam(createParamCentered(Vec(30.0, 195.0), module, Bitshiftgain::LINK_PARAM)); 267 | 268 | // lights 269 | addChild(createLightCentered>(Vec(48, 195), module, Bitshiftgain::LINK_LIGHT)); 270 | 271 | // inputs 272 | addInput(createInputCentered(Vec(30.0, 115.0), module, Bitshiftgain::IN_A_INPUT)); 273 | addInput(createInputCentered(Vec(30.0, 285.0), module, Bitshiftgain::IN_B_INPUT)); 274 | 275 | // outputs 276 | addOutput(createOutputCentered(Vec(30.0, 155.0), module, Bitshiftgain::OUT_A_OUTPUT)); 277 | addOutput(createOutputCentered(Vec(30.0, 325.0), module, Bitshiftgain::OUT_B_OUTPUT)); 278 | } 279 | }; 280 | 281 | Model* modelBitshiftgain = createModel("bitshiftgain"); -------------------------------------------------------------------------------- /src/hombre.cpp: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | Hombre 3 | ------ 4 | VCV Rack module based on Hombre by Chris Johnson from Airwindows 5 | 6 | Ported and designed by Jens Robert Janke 7 | 8 | Changes/Additions: 9 | - mono 10 | - CV inputs for voicing and intensity 11 | - polyphonic 12 | 13 | See ./LICENSE.md for all licenses 14 | ************************************************************************************************/ 15 | 16 | #include "plugin.hpp" 17 | 18 | // quality options 19 | #define ECO 0 20 | #define HIGH 1 21 | 22 | struct Hombre : Module { 23 | enum ParamIds { 24 | VOICING_PARAM, 25 | INTENSITY_PARAM, 26 | NUM_PARAMS 27 | }; 28 | enum InputIds { 29 | VOICING_CV_INPUT, 30 | INTENSITY_CV_INPUT, 31 | IN_INPUT, 32 | NUM_INPUTS 33 | }; 34 | enum OutputIds { 35 | OUT_OUTPUT, 36 | NUM_OUTPUTS 37 | }; 38 | enum LightIds { 39 | NUM_LIGHTS 40 | }; 41 | 42 | // module variables 43 | const double gainCut = 0.03125; 44 | const double gainBoost = 32.0; 45 | int quality; 46 | 47 | // control parameters 48 | float voicingParam; 49 | float intensityParam; 50 | 51 | // state variables (as arrays in order to handle up to 16 polyphonic channels) 52 | double p[16][4001]; 53 | double slide[16]; 54 | int gcount[16]; 55 | long double fpNShape[16]; 56 | 57 | // other variables, which do not need to be updated every cycle 58 | double overallscale; 59 | double target; 60 | int widthA; 61 | int widthB; 62 | double wet; 63 | double dry; 64 | 65 | Hombre() 66 | { 67 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 68 | configParam(VOICING_PARAM, 0.f, 1.f, 0.5f, "Voicing"); 69 | configParam(INTENSITY_PARAM, 0.f, 1.f, 0.5f, "Intensity"); 70 | 71 | quality = loadQuality(); 72 | onReset(); 73 | } 74 | 75 | void onReset() override 76 | { 77 | onSampleRateChange(); 78 | 79 | for (int i = 0; i < 16; i++) { 80 | for (int count = 0; count < 4000; count++) { 81 | p[i][count] = 0.0; 82 | } 83 | gcount[i] = 0; 84 | slide[i] = 0.5; 85 | fpNShape[i] = 0.0; 86 | } 87 | } 88 | 89 | void onSampleRateChange() override 90 | { 91 | float sampleRate = APP->engine->getSampleRate(); 92 | 93 | overallscale = 1.0; 94 | overallscale /= 44100.0; 95 | overallscale *= sampleRate; 96 | 97 | widthA = (int)(1.0 * overallscale); 98 | widthB = (int)(7.0 * overallscale); //max 364 at 44.1, 792 at 96K 99 | } 100 | 101 | json_t* dataToJson() override 102 | { 103 | json_t* rootJ = json_object(); 104 | 105 | // quality 106 | json_object_set_new(rootJ, "quality", json_integer(quality)); 107 | 108 | return rootJ; 109 | } 110 | 111 | void dataFromJson(json_t* rootJ) override 112 | { 113 | // quality 114 | json_t* qualityJ = json_object_get(rootJ, "quality"); 115 | if (qualityJ) 116 | quality = json_integer_value(qualityJ); 117 | } 118 | 119 | void process(const ProcessArgs& args) override 120 | { 121 | if (outputs[OUT_OUTPUT].isConnected()) { 122 | 123 | voicingParam = params[VOICING_PARAM].getValue(); 124 | voicingParam += inputs[VOICING_CV_INPUT].getVoltage() / 5; 125 | voicingParam = clamp(voicingParam, 0.01f, 0.99f); 126 | 127 | intensityParam = params[INTENSITY_PARAM].getValue(); 128 | intensityParam += inputs[INTENSITY_CV_INPUT].getVoltage() / 5; 129 | intensityParam = clamp(intensityParam, 0.01f, 0.99f); 130 | 131 | target = voicingParam; 132 | wet = intensityParam; 133 | dry = 1.0 - wet; 134 | 135 | double offsetA; 136 | double offsetB; 137 | double total; 138 | int count; 139 | long double inputSample; 140 | double drySample; 141 | 142 | // input 143 | int numChannels = std::max(1, inputs[IN_INPUT].getChannels()); 144 | 145 | // for each poly channel 146 | for (int i = 0; i < numChannels; i++) { 147 | 148 | // input 149 | inputSample = inputs[IN_INPUT].getPolyVoltage(i); 150 | 151 | // pad gain 152 | inputSample *= gainCut; 153 | 154 | if (quality == HIGH) { 155 | if (inputSample < 1.2e-38 && -inputSample < 1.2e-38) { 156 | static int noisesource = 0; 157 | //this declares a variable before anything else is compiled. It won't keep assigning 158 | //it to 0 for every sample, it's as if the declaration doesn't exist in this context, 159 | //but it lets me add this denormalization fix in a single place rather than updating 160 | //it in three different locations. The variable isn't thread-safe but this is only 161 | //a random seed and we can share it with whatever. 162 | noisesource = noisesource % 1700021; 163 | noisesource++; 164 | int residue = noisesource * noisesource; 165 | residue = residue % 170003; 166 | residue *= residue; 167 | residue = residue % 17011; 168 | residue *= residue; 169 | residue = residue % 1709; 170 | residue *= residue; 171 | residue = residue % 173; 172 | residue *= residue; 173 | residue = residue % 17; 174 | double applyresidue = residue; 175 | applyresidue *= 0.00000001; 176 | applyresidue *= 0.00000001; 177 | inputSample = applyresidue; 178 | } 179 | } 180 | 181 | drySample = inputSample; 182 | 183 | slide[i] = (slide[i] * 0.9997) + (target * 0.0003); 184 | 185 | //adjust for sample rate 186 | offsetA = ((pow(slide[i], 2)) * 77) + 3.2; 187 | offsetB = (3.85 * offsetA) + 41; 188 | offsetA *= overallscale; 189 | offsetB *= overallscale; 190 | 191 | if (gcount[i] < 1 || gcount[i] > 2000) { 192 | gcount[i] = 2000; 193 | } 194 | count = gcount[i]; 195 | 196 | //double buffer 197 | p[i][count + 2000] = p[i][count] = inputSample; 198 | 199 | count = (int)(gcount[i] + floor(offsetA)); 200 | 201 | total = p[i][count] * 0.391; //less as value moves away from .0 202 | total += p[i][count + widthA]; //we can assume always using this in one way or another? 203 | total += p[i][count + widthA + widthA] * 0.391; //greater as value moves away from .0 204 | 205 | inputSample += ((total * 0.274)); 206 | 207 | count = (int)(gcount[i] + floor(offsetB)); 208 | 209 | total = p[i][count] * 0.918; //less as value moves away from .0 210 | total += p[i][count + widthB]; //we can assume always using this in one way or another? 211 | total += p[i][count + widthB + widthB] * 0.918; //greater as value moves away from .0 212 | 213 | inputSample -= ((total * 0.629)); 214 | 215 | inputSample /= 4; 216 | 217 | //still scrolling through the samples, remember 218 | gcount[i]--; 219 | 220 | if (wet != 1.0) { 221 | inputSample = (inputSample * wet) + (drySample * dry); 222 | } 223 | 224 | if (quality == HIGH) { 225 | //stereo 32 bit dither, made small and tidy. 226 | int expon; 227 | frexpf((float)inputSample, &expon); 228 | long double dither = (rand() / (RAND_MAX * 7.737125245533627e+25)) * pow(2, expon + 62); 229 | inputSample += (dither - fpNShape[i]); 230 | fpNShape[i] = dither; 231 | //end 32 bit dither 232 | } 233 | 234 | // bring gain back up 235 | inputSample *= gainBoost; 236 | 237 | // output 238 | outputs[OUT_OUTPUT].setChannels(numChannels); 239 | outputs[OUT_OUTPUT].setVoltage(inputSample, i); 240 | } 241 | } 242 | } 243 | }; 244 | 245 | struct HombreWidget : ModuleWidget { 246 | 247 | // quality item 248 | struct QualityItem : MenuItem { 249 | Hombre* module; 250 | int quality; 251 | 252 | void onAction(const event::Action& e) override 253 | { 254 | module->quality = quality; 255 | } 256 | 257 | void step() override 258 | { 259 | rightText = (module->quality == quality) ? "✔" : ""; 260 | } 261 | }; 262 | 263 | void appendContextMenu(Menu* menu) override 264 | { 265 | Hombre* module = dynamic_cast(this->module); 266 | assert(module); 267 | 268 | menu->addChild(new MenuSeparator()); // separator 269 | 270 | MenuLabel* qualityLabel = new MenuLabel(); // menu label 271 | qualityLabel->text = "Quality"; 272 | menu->addChild(qualityLabel); 273 | 274 | QualityItem* low = new QualityItem(); // low quality 275 | low->text = "Eco"; 276 | low->module = module; 277 | low->quality = 0; 278 | menu->addChild(low); 279 | 280 | QualityItem* high = new QualityItem(); // high quality 281 | high->text = "High"; 282 | high->module = module; 283 | high->quality = 1; 284 | menu->addChild(high); 285 | } 286 | 287 | HombreWidget(Hombre* module) 288 | { 289 | setModule(module); 290 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/hombre_dark.svg"))); 291 | 292 | // screws 293 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 1.5, 0))); 294 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 1.5, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 295 | 296 | // knobs 297 | addParam(createParamCentered(Vec(30.0, 65.0), module, Hombre::VOICING_PARAM)); 298 | addParam(createParamCentered(Vec(30.0, 125.0), module, Hombre::INTENSITY_PARAM)); 299 | 300 | // inputs 301 | addInput(createInputCentered(Vec(30.0, 205.0), module, Hombre::VOICING_CV_INPUT)); 302 | addInput(createInputCentered(Vec(30.0, 245.0), module, Hombre::INTENSITY_CV_INPUT)); 303 | addInput(createInputCentered(Vec(30.0, 285.0), module, Hombre::IN_INPUT)); 304 | 305 | // outputs 306 | addOutput(createOutputCentered(Vec(30.0, 325.0), module, Hombre::OUT_OUTPUT)); 307 | } 308 | }; 309 | 310 | Model* modelHombre = createModel("hombre"); -------------------------------------------------------------------------------- /src/distance.cpp: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | Distance 3 | -------- 4 | VCV Rack module based on Distance by Chris Johnson from Airwindows 5 | 6 | Ported and designed by Jens Robert Janke 7 | 8 | Changes/Additions: 9 | - mono 10 | - CV inputs for distance and dry/wet 11 | - polyphonic 12 | 13 | See ./LICENSE.md for all licenses 14 | ************************************************************************************************/ 15 | 16 | #include "plugin.hpp" 17 | 18 | // quality options 19 | #define ECO 0 20 | #define HIGH 1 21 | 22 | struct Distance : Module { 23 | enum ParamIds { 24 | DISTANCE_PARAM, 25 | DRYWET_PARAM, 26 | NUM_PARAMS 27 | }; 28 | enum InputIds { 29 | DISTANCE_CV_INPUT, 30 | DRYWET_CV_INPUT, 31 | IN_INPUT, 32 | NUM_INPUTS 33 | }; 34 | enum OutputIds { 35 | OUT_OUTPUT, 36 | NUM_OUTPUTS 37 | }; 38 | enum LightIds { 39 | NUM_LIGHTS 40 | }; 41 | 42 | // module variables 43 | const double gainCut = 0.03125; 44 | const double gainBoost = 32.0; 45 | int quality; 46 | 47 | // control parameters 48 | float distanceParam; 49 | float drywetParam; 50 | 51 | // state variables (as arrays in order to handle up to 16 polyphonic channels) 52 | double lastclamp[16]; 53 | double clasp[16]; 54 | double change[16]; 55 | double thirdresult[16]; 56 | double prevresult[16]; 57 | double last[16]; 58 | long double fpNShape[16]; 59 | 60 | // other variables, which do not need to be updated every cycle 61 | double overallscale; 62 | double softslew; 63 | double filtercorrect; 64 | double thirdfilter; 65 | double levelcorrect; 66 | double wet; 67 | double dry; 68 | float lastDistanceParam; 69 | float lastDrywetParam; 70 | 71 | Distance() 72 | { 73 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 74 | configParam(DISTANCE_PARAM, 0.f, 1.f, 0.f, "Distance"); 75 | configParam(DRYWET_PARAM, 0.f, 1.f, 1.f, "Dry/Wet"); 76 | 77 | quality = loadQuality(); 78 | onReset(); 79 | } 80 | 81 | void onReset() override 82 | { 83 | onSampleRateChange(); 84 | 85 | for (int i = 0; i < 16; i++) { 86 | thirdresult[i] = prevresult[i] = lastclamp[i] = clasp[i] = change[i] = last[i] = 0.0; 87 | fpNShape[i] = 0.0; 88 | } 89 | 90 | softslew = 0.0; 91 | filtercorrect = 0.0; 92 | thirdfilter = 0.0; 93 | levelcorrect = 0.0; 94 | wet = 0.0; 95 | dry = 0.0; 96 | lastDistanceParam = 0.0; 97 | lastDrywetParam = 0.0; 98 | } 99 | 100 | void onSampleRateChange() override 101 | { 102 | float sampleRate = APP->engine->getSampleRate(); 103 | 104 | overallscale = 1.0; 105 | overallscale /= 44100.0; 106 | overallscale *= sampleRate; 107 | } 108 | 109 | json_t* dataToJson() override 110 | { 111 | json_t* rootJ = json_object(); 112 | 113 | // quality 114 | json_object_set_new(rootJ, "quality", json_integer(quality)); 115 | 116 | return rootJ; 117 | } 118 | 119 | void dataFromJson(json_t* rootJ) override 120 | { 121 | // quality 122 | json_t* qualityJ = json_object_get(rootJ, "quality"); 123 | if (qualityJ) 124 | quality = json_integer_value(qualityJ); 125 | } 126 | 127 | void process(const ProcessArgs& args) override 128 | { 129 | if (outputs[OUT_OUTPUT].isConnected()) { 130 | 131 | distanceParam = params[DISTANCE_PARAM].getValue(); 132 | distanceParam += inputs[DISTANCE_CV_INPUT].getVoltage() / 5; 133 | distanceParam = clamp(distanceParam, 0.01f, 0.99f); 134 | 135 | drywetParam = params[DRYWET_PARAM].getValue(); 136 | drywetParam += inputs[DRYWET_CV_INPUT].getVoltage() / 5; 137 | drywetParam = clamp(drywetParam, 0.01f, 0.99f); 138 | 139 | // update only if distanceParam has changed 140 | if (distanceParam != lastDistanceParam) { 141 | softslew = (pow(distanceParam * 2.0, 3.0) * 12.0) + 0.6; 142 | softslew *= overallscale; 143 | filtercorrect = softslew / 2.0; 144 | thirdfilter = softslew / 3.0; 145 | levelcorrect = 1.0 + (softslew / 6.0); 146 | 147 | lastDistanceParam = distanceParam; 148 | } 149 | 150 | // update only if distanceParam has changed 151 | if (drywetParam != lastDrywetParam) { 152 | wet = drywetParam; 153 | dry = 1.0 - wet; 154 | 155 | lastDrywetParam = drywetParam; 156 | } 157 | 158 | double postfilter; 159 | double bridgerectifier; 160 | long double inputSample; 161 | long double drySample; 162 | 163 | // number of polyphonic channels 164 | int numChannels = std::max(1, inputs[IN_INPUT].getChannels()); 165 | 166 | // for each poly channel 167 | for (int i = 0; i < numChannels; i++) { 168 | 169 | // input 170 | inputSample = inputs[IN_INPUT].getPolyVoltage(i); 171 | 172 | // pad gain 173 | inputSample *= gainCut; 174 | 175 | if (quality == HIGH) { 176 | if (inputSample < 1.2e-38 && -inputSample < 1.2e-38) { 177 | static int noisesource = 0; 178 | //this declares a variable before anything else is compiled. It won't keep assigning 179 | //it to 0 for every sample, it's as if the declaration doesn't exist in this context, 180 | //but it lets me add this denormalization fix in a single place rather than updating 181 | //it in three different locations. The variable isn't thread-safe but this is only 182 | //a random seed and we can share it with whatever. 183 | noisesource = noisesource % 1700021; 184 | noisesource++; 185 | int residue = noisesource * noisesource; 186 | residue = residue % 170003; 187 | residue *= residue; 188 | residue = residue % 17011; 189 | residue *= residue; 190 | residue = residue % 1709; 191 | residue *= residue; 192 | residue = residue % 173; 193 | residue *= residue; 194 | residue = residue % 17; 195 | double applyresidue = residue; 196 | applyresidue *= 0.00000001; 197 | applyresidue *= 0.00000001; 198 | inputSample = applyresidue; 199 | } 200 | } 201 | 202 | drySample = inputSample; 203 | 204 | inputSample *= softslew; 205 | lastclamp[i] = clasp[i]; 206 | clasp[i] = inputSample - last[i]; 207 | postfilter = change[i] = fabs(clasp[i] - lastclamp[i]); 208 | postfilter += filtercorrect; 209 | if (change[i] > 1.5707963267949) 210 | change[i] = 1.5707963267949; 211 | bridgerectifier = (1.0 - sin(change[i])); 212 | if (bridgerectifier < 0.0) 213 | bridgerectifier = 0.0; 214 | inputSample = last[i] + (clasp[i] * bridgerectifier); 215 | last[i] = inputSample; 216 | inputSample /= softslew; 217 | inputSample += (thirdresult[i] * thirdfilter); 218 | inputSample /= (thirdfilter + 1.0); 219 | inputSample += (prevresult[i] * postfilter); 220 | inputSample /= (postfilter + 1.0); 221 | //do an IIR like thing to further squish superdistant stuff 222 | thirdresult[i] = prevresult[i]; 223 | prevresult[i] = inputSample; 224 | inputSample *= levelcorrect; 225 | 226 | if (wet < 1.0) { 227 | inputSample = (drySample * dry) + (inputSample * wet); 228 | } 229 | 230 | if (quality == HIGH) { 231 | //stereo 32 bit dither, made small and tidy. 232 | int expon; 233 | frexpf((float)inputSample, &expon); 234 | long double dither = (rand() / (RAND_MAX * 7.737125245533627e+25)) * pow(2, expon + 62); 235 | inputSample += (dither - fpNShape[i]); 236 | fpNShape[i] = dither; 237 | //end 32 bit dither 238 | } 239 | 240 | // bring gain back up 241 | inputSample *= gainBoost; 242 | 243 | // output 244 | outputs[OUT_OUTPUT].setChannels(numChannels); 245 | outputs[OUT_OUTPUT].setVoltage(inputSample, i); 246 | } 247 | } 248 | } 249 | }; 250 | 251 | struct DistanceWidget : ModuleWidget { 252 | 253 | // quality item 254 | struct QualityItem : MenuItem { 255 | Distance* module; 256 | int quality; 257 | 258 | void onAction(const event::Action& e) override 259 | { 260 | module->quality = quality; 261 | } 262 | 263 | void step() override 264 | { 265 | rightText = (module->quality == quality) ? "✔" : ""; 266 | } 267 | }; 268 | 269 | void appendContextMenu(Menu* menu) override 270 | { 271 | Distance* module = dynamic_cast(this->module); 272 | assert(module); 273 | 274 | menu->addChild(new MenuSeparator()); // separator 275 | 276 | MenuLabel* qualityLabel = new MenuLabel(); // menu label 277 | qualityLabel->text = "Quality"; 278 | menu->addChild(qualityLabel); 279 | 280 | QualityItem* low = new QualityItem(); // low quality 281 | low->text = "Eco"; 282 | low->module = module; 283 | low->quality = 0; 284 | menu->addChild(low); 285 | 286 | QualityItem* high = new QualityItem(); // high quality 287 | high->text = "High"; 288 | high->module = module; 289 | high->quality = 1; 290 | menu->addChild(high); 291 | } 292 | 293 | DistanceWidget(Distance* module) 294 | { 295 | setModule(module); 296 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/distance_dark.svg"))); 297 | 298 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 1.5, 0))); 299 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 1.5, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 300 | 301 | addParam(createParamCentered(Vec(30.0, 65.0), module, Distance::DISTANCE_PARAM)); 302 | addParam(createParamCentered(Vec(30.0, 125.0), module, Distance::DRYWET_PARAM)); 303 | 304 | addInput(createInputCentered(Vec(30.0, 205.0), module, Distance::DISTANCE_CV_INPUT)); 305 | addInput(createInputCentered(Vec(30.0, 245.0), module, Distance::DRYWET_CV_INPUT)); 306 | addInput(createInputCentered(Vec(30.0, 285.0), module, Distance::IN_INPUT)); 307 | 308 | addOutput(createOutputCentered(Vec(30.0, 325.0), module, Distance::OUT_OUTPUT)); 309 | } 310 | }; 311 | 312 | Model* modelDistance = createModel("distance"); -------------------------------------------------------------------------------- /src/plugin.cpp: -------------------------------------------------------------------------------- 1 | #include "plugin.hpp" 2 | 3 | Plugin* pluginInstance; 4 | 5 | void init(Plugin* p) 6 | { 7 | pluginInstance = p; 8 | 9 | // Add modules here 10 | // p->addModel(modelAcceleration); 11 | p->addModel(modelBitshiftgain); 12 | p->addModel(modelCapacitor); 13 | p->addModel(modelCapacitor_stereo); 14 | p->addModel(modelChorus); 15 | p->addModel(modelConsole); 16 | p->addModel(modelConsole_mm); 17 | p->addModel(modelDistance); 18 | p->addModel(modelGolem); 19 | p->addModel(modelHolt); 20 | p->addModel(modelHombre); 21 | p->addModel(modelInterstage); 22 | p->addModel(modelMonitoring); 23 | p->addModel(modelMv); 24 | p->addModel(modelRasp); 25 | p->addModel(modelReseq); 26 | p->addModel(modelTape); 27 | p->addModel(modelTremolo); 28 | p->addModel(modelVibrato); 29 | 30 | // Any other plugin initialization may go here. 31 | // As an alternative, consider lazy-loading assets and lookup tables when your module is created to reduce startup times of Rack. 32 | } 33 | 34 | /* Other stuff */ 35 | 36 | /* #quality mode 37 | ======================================================================================== */ 38 | void saveQuality(bool quality) 39 | { 40 | json_t* settingsJ = json_object(); 41 | json_object_set_new(settingsJ, "quality", json_boolean(quality)); 42 | std::string settingsFilename = asset::user("Rackwindows.json"); 43 | FILE* file = fopen(settingsFilename.c_str(), "w"); 44 | if (file) { 45 | json_dumpf(settingsJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); 46 | fclose(file); 47 | } 48 | json_decref(settingsJ); 49 | } 50 | 51 | bool loadQuality() 52 | { 53 | bool ret = false; 54 | std::string settingsFilename = asset::user("Rackwindows.json"); 55 | FILE* file = fopen(settingsFilename.c_str(), "r"); 56 | if (!file) { 57 | saveQuality(false); 58 | return ret; 59 | } 60 | json_error_t error; 61 | json_t* settingsJ = json_loadf(file, 0, &error); 62 | if (!settingsJ) { 63 | // invalid setting json file 64 | fclose(file); 65 | saveQuality(false); 66 | return ret; 67 | } 68 | json_t* qualityJ = json_object_get(settingsJ, "quality"); 69 | if (qualityJ) 70 | ret = json_boolean_value(qualityJ); 71 | 72 | fclose(file); 73 | json_decref(settingsJ); 74 | return ret; 75 | } 76 | 77 | void saveHighQualityAsDefault(bool highQualityAsDefault) 78 | { 79 | json_t* settingsJ = json_object(); 80 | json_object_set_new(settingsJ, "highQualityAsDefault", json_boolean(highQualityAsDefault)); 81 | std::string settingsFilename = asset::user("Rackwindows.json"); 82 | FILE* file = fopen(settingsFilename.c_str(), "w"); 83 | if (file) { 84 | json_dumpf(settingsJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); 85 | fclose(file); 86 | } 87 | json_decref(settingsJ); 88 | } 89 | 90 | // https://github.com/MarcBoule/Geodesics/blob/master/src/Geodesics.cpp 91 | bool loadHighQualityAsDefault() 92 | { 93 | bool ret = false; 94 | std::string settingsFilename = asset::user("Rackwindows.json"); 95 | FILE* file = fopen(settingsFilename.c_str(), "r"); 96 | if (!file) { 97 | saveHighQualityAsDefault(false); 98 | return ret; 99 | } 100 | json_error_t error; 101 | json_t* settingsJ = json_loadf(file, 0, &error); 102 | if (!settingsJ) { 103 | // invalid setting json file 104 | fclose(file); 105 | saveHighQualityAsDefault(false); 106 | return ret; 107 | } 108 | json_t* highQualityAsDefaultJ = json_object_get(settingsJ, "highQualityAsDefault"); 109 | if (highQualityAsDefaultJ) 110 | ret = json_boolean_value(highQualityAsDefaultJ); 111 | 112 | fclose(file); 113 | json_decref(settingsJ); 114 | return ret; 115 | } 116 | 117 | /* #console type (Console, Console MM) 118 | ======================================================================================== */ 119 | void saveConsoleType(int consoleType) 120 | { 121 | json_t* settingsJ = json_object(); 122 | json_object_set_new(settingsJ, "consoleType", json_boolean(consoleType)); 123 | std::string settingsFilename = asset::user("Rackwindows.json"); 124 | FILE* file = fopen(settingsFilename.c_str(), "w"); 125 | if (file) { 126 | json_dumpf(settingsJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); 127 | fclose(file); 128 | } 129 | json_decref(settingsJ); 130 | } 131 | 132 | int loadConsoleType() 133 | { 134 | bool ret = false; 135 | std::string settingsFilename = asset::user("Rackwindows.json"); 136 | FILE* file = fopen(settingsFilename.c_str(), "r"); 137 | if (!file) { 138 | saveConsoleType(false); 139 | return ret; 140 | } 141 | json_error_t error; 142 | json_t* settingsJ = json_loadf(file, 0, &error); 143 | if (!settingsJ) { 144 | // invalid setting json file 145 | fclose(file); 146 | saveConsoleType(false); 147 | return ret; 148 | } 149 | json_t* consoleTypeJ = json_object_get(settingsJ, "consoleType"); 150 | if (consoleTypeJ) 151 | ret = json_boolean_value(consoleTypeJ); 152 | 153 | fclose(file); 154 | json_decref(settingsJ); 155 | return ret; 156 | } 157 | 158 | /* #slew type (Rasp) 159 | ======================================================================================== */ 160 | void saveSlewType(int slewType) 161 | { 162 | json_t* settingsJ = json_object(); 163 | json_object_set_new(settingsJ, "slewType", json_integer(slewType)); 164 | std::string settingsFilename = asset::user("Rackwindows.json"); 165 | FILE* file = fopen(settingsFilename.c_str(), "w"); 166 | if (file) { 167 | json_dumpf(settingsJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); 168 | fclose(file); 169 | } 170 | json_decref(settingsJ); 171 | } 172 | 173 | int loadSlewType() 174 | { 175 | bool ret = false; 176 | std::string settingsFilename = asset::user("Rackwindows.json"); 177 | FILE* file = fopen(settingsFilename.c_str(), "r"); 178 | if (!file) { 179 | saveSlewType(false); 180 | return ret; 181 | } 182 | json_error_t error; 183 | json_t* settingsJ = json_loadf(file, 0, &error); 184 | if (!settingsJ) { 185 | // invalid setting json file 186 | fclose(file); 187 | saveSlewType(false); 188 | return ret; 189 | } 190 | json_t* slewTypeJ = json_object_get(settingsJ, "slewType"); 191 | if (slewTypeJ) 192 | ret = json_integer_value(slewTypeJ); 193 | 194 | fclose(file); 195 | json_decref(settingsJ); 196 | return ret; 197 | } 198 | 199 | /* #direct output mode (Console MM) 200 | ======================================================================================== */ 201 | void saveDirectOutMode(bool directOutMode) 202 | { 203 | json_t* settingsJ = json_object(); 204 | json_object_set_new(settingsJ, "directOutMode", json_boolean(directOutMode)); 205 | std::string settingsFilename = asset::user("Rackwindows.json"); 206 | FILE* file = fopen(settingsFilename.c_str(), "w"); 207 | if (file) { 208 | json_dumpf(settingsJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); 209 | fclose(file); 210 | } 211 | json_decref(settingsJ); 212 | } 213 | 214 | int loadDirectOutMode() 215 | { 216 | bool ret = false; 217 | std::string settingsFilename = asset::user("Rackwindows.json"); 218 | FILE* file = fopen(settingsFilename.c_str(), "r"); 219 | if (!file) { 220 | saveDirectOutMode(false); 221 | return ret; 222 | } 223 | json_error_t error; 224 | json_t* settingsJ = json_loadf(file, 0, &error); 225 | if (!settingsJ) { 226 | // invalid setting json file 227 | fclose(file); 228 | saveDirectOutMode(false); 229 | return ret; 230 | } 231 | json_t* directOutModeJ = json_object_get(settingsJ, "directOutMode"); 232 | if (directOutModeJ) 233 | ret = json_boolean_value(directOutModeJ); 234 | 235 | fclose(file); 236 | json_decref(settingsJ); 237 | return ret; 238 | } 239 | 240 | /* #delay mode (Golem) 241 | ======================================================================================== */ 242 | void saveDelayMode(int delayMode) 243 | { 244 | json_t* settingsJ = json_object(); 245 | json_object_set_new(settingsJ, "delayMode", json_boolean(delayMode)); 246 | std::string settingsFilename = asset::user("Rackwindows.json"); 247 | FILE* file = fopen(settingsFilename.c_str(), "w"); 248 | if (file) { 249 | json_dumpf(settingsJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); 250 | fclose(file); 251 | } 252 | json_decref(settingsJ); 253 | } 254 | 255 | int loadDelayMode() 256 | { 257 | bool ret = false; 258 | std::string settingsFilename = asset::user("Rackwindows.json"); 259 | FILE* file = fopen(settingsFilename.c_str(), "r"); 260 | if (!file) { 261 | saveDelayMode(false); 262 | return ret; 263 | } 264 | json_error_t error; 265 | json_t* settingsJ = json_loadf(file, 0, &error); 266 | if (!settingsJ) { 267 | // invalid setting json file 268 | fclose(file); 269 | saveDelayMode(false); 270 | return ret; 271 | } 272 | json_t* delayModeJ = json_object_get(settingsJ, "delayMode"); 273 | if (delayModeJ) 274 | ret = json_boolean_value(delayModeJ); 275 | 276 | fclose(file); 277 | json_decref(settingsJ); 278 | return ret; 279 | } 280 | 281 | /* #themes 282 | ======================================================================================== */ 283 | // https://github.com/MarcBoule/Geodesics/blob/master/src/Geodesics.cpp 284 | void saveDarkAsDefault(bool darkAsDefault) 285 | { 286 | json_t* settingsJ = json_object(); 287 | json_object_set_new(settingsJ, "darkAsDefault", json_boolean(darkAsDefault)); 288 | std::string settingsFilename = asset::user("Rackwindows.json"); 289 | FILE* file = fopen(settingsFilename.c_str(), "w"); 290 | if (file) { 291 | json_dumpf(settingsJ, file, JSON_INDENT(2) | JSON_REAL_PRECISION(9)); 292 | fclose(file); 293 | } 294 | json_decref(settingsJ); 295 | } 296 | 297 | // https://github.com/MarcBoule/Geodesics/blob/master/src/Geodesics.cpp 298 | bool loadDarkAsDefault() 299 | { 300 | bool ret = false; 301 | std::string settingsFilename = asset::user("Rackwindows.json"); 302 | FILE* file = fopen(settingsFilename.c_str(), "r"); 303 | if (!file) { 304 | saveDarkAsDefault(false); 305 | return ret; 306 | } 307 | json_error_t error; 308 | json_t* settingsJ = json_loadf(file, 0, &error); 309 | if (!settingsJ) { 310 | // invalid setting json file 311 | fclose(file); 312 | saveDarkAsDefault(false); 313 | return ret; 314 | } 315 | json_t* darkAsDefaultJ = json_object_get(settingsJ, "darkAsDefault"); 316 | if (darkAsDefaultJ) 317 | ret = json_boolean_value(darkAsDefaultJ); 318 | 319 | fclose(file); 320 | json_decref(settingsJ); 321 | return ret; 322 | } 323 | 324 | // https://github.com/ValleyAudio/ValleyRackFree/blob/v1.0/src/Common/DSP/NonLinear.hpp 325 | inline float tanhDriveSignal(float x, float drive) 326 | { 327 | x *= drive; 328 | 329 | if (x < -1.3f) { 330 | return -1.f; 331 | } else if (x < -0.75f) { 332 | return (x * x + 2.6f * x + 1.69f) * 0.833333f - 1.f; 333 | } else if (x > 1.3f) { 334 | return 1.f; 335 | } else if (x > 0.75f) { 336 | return 1.f - (x * x - 2.6f * x + 1.69f) * 0.833333f; 337 | } 338 | return x; 339 | } 340 | -------------------------------------------------------------------------------- /src/rasp.cpp: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | Rasp 3 | ---- 4 | VCV Rack module based on Slew/Slew2/Slew3 and Acceleration by Chris Johnson from Airwindows 5 | 6 | Ported and designed by Jens Robert Janke 7 | 8 | Changes/Additions: 9 | - mono 10 | - cv inputs for clamp and limit 11 | - clamp and limit outputs normalled to each other 12 | - slew algorithm selectable via menu 13 | - polyphonic 14 | 15 | See ./LICENSE.md for all licenses 16 | ************************************************************************************************/ 17 | 18 | #include "plugin.hpp" 19 | 20 | // quality options 21 | #define ECO 0 22 | #define HIGH 1 23 | 24 | // slew types 25 | #define SLEW2 0 26 | #define SLEW 1 27 | #define SLEW3 2 28 | 29 | // polyphony 30 | #define MAX_POLY_CHANNELS 16 31 | 32 | struct Rasp : Module { 33 | enum ParamIds { 34 | CLAMP_PARAM, 35 | LIMIT_PARAM, 36 | NUM_PARAMS 37 | }; 38 | enum InputIds { 39 | CLAMP_CV_INPUT, 40 | LIMIT_CV_INPUT, 41 | IN_INPUT, 42 | NUM_INPUTS 43 | }; 44 | enum OutputIds { 45 | CLAMP_OUTPUT, 46 | LIMIT_OUTPUT, 47 | NUM_OUTPUTS 48 | }; 49 | enum LightIds { 50 | NUM_LIGHTS 51 | }; 52 | 53 | // module variables 54 | const double gainCut = 0.1; 55 | const double gainBoost = 10.0; 56 | bool quality; 57 | int slewType; 58 | 59 | // control parameters 60 | float clampParam; 61 | float limitParam; 62 | 63 | // state variables 64 | rwlib::Slew slew[MAX_POLY_CHANNELS]; 65 | rwlib::Slew2 slew2[MAX_POLY_CHANNELS]; 66 | rwlib::Slew3 slew3[MAX_POLY_CHANNELS]; 67 | rwlib::Acceleration acceleration[MAX_POLY_CHANNELS]; 68 | long double fpNShapeClamp[MAX_POLY_CHANNELS]; 69 | long double fpNShapeLimit[MAX_POLY_CHANNELS]; 70 | 71 | // other 72 | double overallscale; 73 | 74 | Rasp() 75 | { 76 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 77 | configParam(CLAMP_PARAM, 0.f, 1.f, 0.f, "Clamp", " %", 0.f, 100.f); 78 | configParam(LIMIT_PARAM, 0.f, 1.f, 0.f, "Limit", " %", 0.f, 100.f); 79 | 80 | quality = ECO; 81 | slewType = SLEW2; 82 | onReset(); 83 | } 84 | 85 | void onReset() override 86 | { 87 | onSampleRateChange(); 88 | 89 | clampParam = 0.f; 90 | limitParam = 0.f; 91 | 92 | for (int i = 0; i < MAX_POLY_CHANNELS; i++) { 93 | { 94 | slew[i] = rwlib::Slew(); 95 | slew2[i] = rwlib::Slew2(); 96 | slew3[i] = rwlib::Slew3(); 97 | acceleration[i] = rwlib::Acceleration(); 98 | fpNShapeClamp[i] = 0.0; 99 | fpNShapeLimit[i] = 0.0; 100 | } 101 | } 102 | } 103 | 104 | void onSampleRateChange() override 105 | { 106 | float sampleRate = APP->engine->getSampleRate(); 107 | 108 | overallscale = 1.0; 109 | overallscale /= 44100.0; 110 | overallscale *= sampleRate; 111 | } 112 | 113 | json_t* dataToJson() override 114 | { 115 | json_t* rootJ = json_object(); 116 | 117 | // quality 118 | json_object_set_new(rootJ, "quality", json_integer(quality)); 119 | 120 | // slew type 121 | json_object_set_new(rootJ, "slewType", json_integer(slewType)); 122 | 123 | return rootJ; 124 | } 125 | 126 | void dataFromJson(json_t* rootJ) override 127 | { 128 | // quality 129 | json_t* qualityJ = json_object_get(rootJ, "quality"); 130 | if (qualityJ) 131 | quality = json_integer_value(qualityJ); 132 | 133 | // slew type 134 | json_t* slewTypeJ = json_object_get(rootJ, "slewType"); 135 | if (slewTypeJ) 136 | slewType = json_integer_value(slewTypeJ); 137 | } 138 | 139 | void process(const ProcessArgs& args) override 140 | { 141 | // get params 142 | limitParam = params[LIMIT_PARAM].getValue(); 143 | limitParam += inputs[LIMIT_CV_INPUT].getVoltage() / 5; 144 | limitParam = clamp(limitParam, 0.f, 1.f); 145 | 146 | clampParam = params[CLAMP_PARAM].getValue(); 147 | clampParam += inputs[CLAMP_CV_INPUT].getVoltage() / 5; 148 | clampParam = clamp(clampParam, 0.f, 1.f); 149 | 150 | long double inputSample; 151 | long double clampSample = 0.0; 152 | long double limitSample = 0.0; 153 | 154 | // for each poly channel 155 | for (int i = 0, numChannels = std::max(1, inputs[IN_INPUT].getChannels()); i < numChannels; ++i) { 156 | 157 | // get input 158 | inputSample = inputs[IN_INPUT].getPolyVoltage(i); 159 | 160 | // pad gain 161 | inputSample *= gainCut; 162 | 163 | if (quality == HIGH) { 164 | inputSample = rwlib::denormalize(inputSample); 165 | } 166 | 167 | // work the magic 168 | if (outputs[CLAMP_OUTPUT].isConnected()) { 169 | if (outputs[LIMIT_OUTPUT].isConnected()) { 170 | switch (slewType) { 171 | case SLEW: 172 | clampSample = slew[i].process(inputSample, clampParam, overallscale); 173 | break; 174 | case SLEW2: 175 | clampSample = slew2[i].process(inputSample, clampParam, overallscale); 176 | break; 177 | case SLEW3: 178 | clampSample = slew3[i].process(inputSample, clampParam, overallscale); 179 | } 180 | } else { 181 | limitSample = acceleration[i].process(inputSample, limitParam, 1.f, overallscale); 182 | switch (slewType) { 183 | case SLEW: 184 | clampSample = slew[i].process(limitSample, clampParam, overallscale); 185 | break; 186 | case SLEW2: 187 | clampSample = slew2[i].process(limitSample, clampParam, overallscale); 188 | break; 189 | case SLEW3: 190 | clampSample = slew3[i].process(limitSample, clampParam, overallscale); 191 | } 192 | } 193 | } 194 | if (outputs[LIMIT_OUTPUT].isConnected()) { 195 | if (outputs[CLAMP_OUTPUT].isConnected()) { 196 | limitSample = acceleration[i].process(inputSample, limitParam, 1.f, overallscale); 197 | } else { 198 | switch (slewType) { 199 | case SLEW: 200 | clampSample = slew[i].process(inputSample, clampParam, overallscale); 201 | break; 202 | case SLEW2: 203 | clampSample = slew2[i].process(inputSample, clampParam, overallscale); 204 | break; 205 | case SLEW3: 206 | clampSample = slew3[i].process(inputSample, clampParam, overallscale); 207 | } 208 | limitSample = acceleration[i].process(clampSample, limitParam, 1.f, overallscale); 209 | } 210 | } 211 | 212 | if (quality == HIGH) { 213 | // 32 bit dither, made small and tidy. 214 | int expon; 215 | frexpf((float)clampSample, &expon); 216 | long double dither = (rand() / (RAND_MAX * 7.737125245533627e+25)) * pow(2, expon + 62); 217 | clampSample += (dither - fpNShapeClamp[i]); 218 | fpNShapeClamp[i] = dither; 219 | frexpf((float)limitSample, &expon); 220 | dither = (rand() / (RAND_MAX * 7.737125245533627e+25)) * pow(2, expon + 62); 221 | limitSample += (dither - fpNShapeLimit[i]); 222 | fpNShapeLimit[i] = dither; 223 | } 224 | 225 | // bring levels back up 226 | clampSample *= gainBoost; 227 | limitSample *= gainBoost; 228 | 229 | // output 230 | outputs[CLAMP_OUTPUT].setChannels(numChannels); 231 | outputs[CLAMP_OUTPUT].setVoltage(clampSample, i); 232 | outputs[LIMIT_OUTPUT].setChannels(numChannels); 233 | outputs[LIMIT_OUTPUT].setVoltage(limitSample, i); 234 | } 235 | } 236 | }; 237 | 238 | struct RaspWidget : ModuleWidget { 239 | 240 | // quality item 241 | struct QualityItem : MenuItem { 242 | Rasp* module; 243 | int quality; 244 | 245 | void onAction(const event::Action& e) override 246 | { 247 | module->quality = quality; 248 | } 249 | 250 | void step() override 251 | { 252 | rightText = (module->quality == quality) ? "✔" : ""; 253 | } 254 | }; 255 | 256 | // slew type item 257 | struct SlewTypeItem : MenuItem { 258 | Rasp* module; 259 | int slewType; 260 | 261 | void onAction(const event::Action& e) override 262 | { 263 | module->slewType = slewType; 264 | } 265 | 266 | void step() override 267 | { 268 | rightText = (module->slewType == slewType) ? "✔" : ""; 269 | } 270 | }; 271 | 272 | void appendContextMenu(Menu* menu) override 273 | { 274 | Rasp* module = dynamic_cast(this->module); 275 | assert(module); 276 | 277 | menu->addChild(new MenuSeparator()); // separator 278 | 279 | MenuLabel* qualityLabel = new MenuLabel(); // menu label 280 | qualityLabel->text = "Quality"; 281 | menu->addChild(qualityLabel); 282 | 283 | QualityItem* low = new QualityItem(); // low quality 284 | low->text = "Eco"; 285 | low->module = module; 286 | low->quality = 0; 287 | menu->addChild(low); 288 | 289 | QualityItem* high = new QualityItem(); // high quality 290 | high->text = "High"; 291 | high->module = module; 292 | high->quality = 1; 293 | menu->addChild(high); 294 | 295 | menu->addChild(new MenuSeparator()); // separator 296 | 297 | MenuLabel* slewLabel = new MenuLabel(); // menu label 298 | slewLabel->text = "Clamp Type"; 299 | menu->addChild(slewLabel); 300 | 301 | SlewTypeItem* slew = new SlewTypeItem(); // Slew 302 | slew->text = "Slew"; 303 | slew->module = module; 304 | slew->slewType = SLEW; 305 | menu->addChild(slew); 306 | 307 | SlewTypeItem* slew2 = new SlewTypeItem(); // Slew2 308 | slew2->text = "Slew2"; 309 | slew2->module = module; 310 | slew2->slewType = SLEW2; 311 | menu->addChild(slew2); 312 | 313 | SlewTypeItem* slew3 = new SlewTypeItem(); // Slew3 314 | slew3->text = "Slew3"; 315 | slew3->module = module; 316 | slew3->slewType = SLEW3; 317 | menu->addChild(slew3); 318 | } 319 | 320 | RaspWidget(Rasp* module) 321 | { 322 | setModule(module); 323 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/rasp_dark.svg"))); 324 | 325 | // screws 326 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 1.5, 0))); 327 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 1.5, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 328 | 329 | //params 330 | addParam(createParamCentered(Vec(30.0, 65.0), module, Rasp::CLAMP_PARAM)); 331 | addParam(createParamCentered(Vec(30.0, 115.0), module, Rasp::LIMIT_PARAM)); 332 | 333 | // inputs 334 | addInput(createInputCentered(Vec(30.0, 165.0), module, Rasp::CLAMP_CV_INPUT)); 335 | addInput(createInputCentered(Vec(30.0, 205.0), module, Rasp::LIMIT_CV_INPUT)); 336 | addInput(createInputCentered(Vec(30.0, 245.0), module, Rasp::IN_INPUT)); 337 | 338 | // outputs 339 | addOutput(createOutputCentered(Vec(30.0, 285.0), module, Rasp::CLAMP_OUTPUT)); 340 | addOutput(createOutputCentered(Vec(30.0, 325.0), module, Rasp::LIMIT_OUTPUT)); 341 | } 342 | }; 343 | 344 | Model* modelRasp = createModel("rasp"); -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # A collection of VCV Rack modules based on audio plugins by Chris Johnson from Airwindows 2 | 3 | ![Rackwindows Modules](res/images/rackwindows_panels.jpg) 4 | 5 | Most of these modules are straight-up ports of [Airwindows](http://www.airwindows.com) plugins with the addition of a panel (designed to be potentially buildable in hardware) and the occasional tweak or enhancement to take advantage of the modular environment. 6 | 7 | Airwindows plugins are known for exceptional sound quality, innovative approaches and minimalist interfaces. They often focus on and excel at one specific task, which is why I think they are a natural fit for Rack. 8 | 9 | For more in-depth information about the inner workings of a particular Airwindows plugin please check Chris' [website](http://www.airwindows.com). There is a video on every plugin along with a description and I encourage everyone to explore his body of work. Chances are you stumble upon other gems here and there. He's got a [Patreon](https://www.patreon.com/airwindows) in case you appreciate and want to support what he does. 10 | 11 | **NOTE:** Most modules act a bit differently depending on the selected sample rate, e.g. varying center frequencies on filters. Keep in mind that a number of Airwindows plugins are actually sample rate dependent, therefore in some cases it might be a feature, not a bug. 12 | 13 | ## Licence 14 | 15 | See [LICENSE.md](LICENSE.md) for all licenses 16 | 17 | ## Modules 18 | 19 | - [Capacitor Mono/Stereo](#capacitor): Filters 20 | - [Chorus](#chorus): Chorus with multi-tap option (Ensemble) 21 | - [Console](#console): Stereo summing mixer 22 | - [Console MM](#consolemm): Stereo summing mixer to work in conjunction with MindMeld's MixMaster 23 | - [Distance](#distance): Designed to mimic through-air high frequency attenuation 24 | - [Dual BSG](#dual-bsg): Dual gain shifter 25 | - [Golem](#golem): Micro-delayable crossfader 26 | - [Holt](#holt): Resonant lowpass filter focussed on low frequencies 27 | - [Hombre](#hombre): Texas tone and texture 28 | - [Interstage](#interstage): Subtle analogifier 29 | - [Monitoring](#monitoring): Mix checker 30 | - [MV](#mv): Dual-mono reverb 31 | - [Rasp](#rasp): De-Edger, slew and acceleration limiter 32 | - [ResEQ](#reseq): Resonance equalizer 33 | - [Tape](#tape): All-purpose tape mojo 34 | - [Tremolo](#tremolo): Organic pulsating thing 35 | - [Vibrato](#vibrato): FM Vibrator o_O 36 | 37 | ## Capacitor 38 | 39 | Filters 40 | 41 | ![Rackwindows Capacitor](res/images/capacitor_panels.jpg) 42 | 43 | High/Lowpass filters that come in both mono and stereo, with the stereo version featuring an additional dry/wet control. 44 | 45 | [More information](http://www.airwindows.com/capacitor) 46 | 47 | ## Chorus 48 | 49 | Chorus with multi-tap option (Ensemble) 50 | 51 | ![Rackwindows Chorus](res/images/chorus_panels.jpg) 52 | 53 | This surprisingly versatile module is a combination of Airwindows Chorus and ChorusEnsemble. The switch lets you toggle between the two flavours. 54 | 55 | [More information](http://www.airwindows.com/chorus-vst) 56 | 57 | ## Console 58 | 59 | Stereo summing mixer 60 | 61 | ![Rackwindows Console](res/images/console_panels.jpg) 62 | 63 | Airwindows Console systems sum signals in a way where the resulting soundstage appears less flat, more spacious and more in line with what we are used to from analog mixers. There are a number of variations/flavours of the console concept and this module currently implements the most recent one, Console6, and PurestConsole, which can be selected in the context menu. 64 | 65 | Please check the link below for further information on how exactly this effect is achieved. 66 | 67 | **NOTE**: Due to how the encoding/decoding works, spaciousness and definition will increase with each additional channel. There will be **no effect on a single channel** at all. 68 | 69 | [More information](http://www.airwindows.com/console2) 70 | 71 | ## Console MM 72 | 73 | This is a special version of Console designed to work alongside and interconnected with MindMeld's MixMaster module. It takes MM's polyphonic direct outs and sums the individual channels according to the selected console type. The stereo outputs can then be routed back into the chain inputs of MM to complete the roundtrip (make sure to check "Solo Chain Input" in MM, rightclick on Master). This setup conveniently provides "analogish" summing while making use of all of MM's fantastic mixing capabilities and goodies. 74 | 75 | For added flexibility and in order to honour Chris' spirit of allowing for settings beyond obvious sweetspots, the input gain can be adjusted, or - as a friend put it - taken from "can`t hear shit happening" to overdriven. 76 | 77 | The 3 direct outputs pass on the respective input signal either unprocessed (polyphonic) or summed (mono). 78 | 79 | ## Distance 80 | 81 | Designed to mimic through-air high frequency attenuation 82 | 83 | ![Rackwindows Distance](res/images/distance_panels.jpg) 84 | 85 | Pushes things back. Also quite nice on reverb returns. 86 | 87 | [More information](http://www.airwindows.com/distance-vst) 88 | 89 | ## Dual BSG 90 | 91 | Dual gain shifter 92 | 93 | ![Rackwindows Dual BSG](res/images/dual_bsg_panels.jpg) 94 | 95 | Scales a signal up or down by increments of exactly 6 dB. If no input is connected, the respective output will provide constant voltage selectable in 1V steps from -8V to +8V. The lower section can be linked to the upper one to automatically compensate for values set by the upper 'Shift' knob. If linked the lower 'Shift' knob can be used to offset the signal in 6db steps (input connected) or 1V steps (input not connected). 96 | 97 | [More information](http://www.airwindows.com/bitshiftgain) 98 | 99 | ## Golem 100 | 101 | Micro-delayable crossfader 102 | 103 | ![Rackwindows Golem](res/images/golem_panels.jpg) 104 | 105 | [More information](https://www.airwindows.com/golem-vst/) 106 | 107 | ## Holt 108 | 109 | Resonant lowpass filter focussed on low frequencies 110 | 111 | ![Rackwindows Holt](res/images/holt_panels.jpg) 112 | 113 | Interestingly, Holt's algorithm is based on an Excel method for predicting sales figures based on trends. The result is a lowpass filter that's polite in the highs and increasingly mean towards the low-end. It also allows for seamless morphing between no poles (dry) and 4-poles (24db per octave). An additional output saturation stage helps keeping the possibly massive resonances in check (it can still get nasty though, you have been warned). 114 | 115 | [More information](http://www.airwindows.com/holt) 116 | 117 | ## Hombre 118 | 119 | Texas tone and texture 120 | 121 | ![Rackwindows Hombre](res/images/hombre_panels.jpg) 122 | 123 | [More information](http://www.airwindows.com/hombre-vst) 124 | 125 | ## Interstage 126 | 127 | Subtle analogifier 128 | 129 | ![Rackwindows Interstage](res/images/interstage_panels.jpg) 130 | 131 | Well, I won't attempt to describe or even explain what this one does. Please head straight over to the Airwindows site for details. But I will say this: If I had to choose just one Airwindows tool, this would be it (or maybe Console?). It's simple, it's subtle and to my ears pure class. Put it anywhere in your patch, you can do no wrong. It will most likely be for the better :-) 132 | 133 | [More information](https://www.airwindows.com/interstage/) 134 | 135 | ## Monitoring 136 | 137 | Mix checker 138 | 139 | ![Rackwindows Monitoring](res/images/monitoring_panels.jpg) 140 | 141 | Monitoring is meant to be the last module before the output. It offers a number of tools to help investigate your patch and check for problems. It is calibrated in such a way that modes like *Subs* or *Peaks* will tell you unmistakably when you've got too much (or not enough) energy in a particular area. 142 | Additionally, it features crossfeed for headphones and optional 24/16 bit dither. 143 | 144 | ### Processing modes: 145 | 146 | - **Subs**: This is [SubsOnly](https://www.airwindows.com/slewonly-subsonly/). Useful for checking sub-bass elements. 147 | - **Slew**: This is [SlewOnly](https://www.airwindows.com/slewonly-subsonly/). Similar to *Subs*, but for high frequency content. 148 | - **Peaks**: This is [PeaksOnly](https://www.airwindows.com/peaksonly/). Exposes bursts of excessive energy by transforming them into recognizable signals. 149 | - **Mono**: Mid or sum of the stereo channels. 150 | - **Side**: Just the side information or difference of the stereo channels. 151 | - **Vinyl**: Rolls off the extreme ends of the frequency spectrum. 152 | - **Aurat**: Same as Vinyl, but slightly narrower band. 153 | - **Phone**: Same as above, but significantly less bass and mono. 154 | 155 | ### Can modes: 156 | 157 | These are somewhat sophisticated crossfeed modes for headphones. They go from relatively subtle (A) to quite obvious (D). 158 | 159 | ### Dither: 160 | 161 | You can choose between 24 and 16 bit dithering. The algorithm is [Dark](https://www.airwindows.com/dark/). 162 | 163 | [More information](https://www.airwindows.com/monitoring/) 164 | 165 | ## MV 166 | 167 | Dual-mono reverb 168 | 169 | ![Rackwindows MV](res/images/mv_panels.jpg) 170 | 171 | A reverb based on Bitshiftgain and old Alesis Midiverbs. Capable of turning everything into a pad or sustaining a 'bloom' forever. Watch your volume when using small amounts of depth with full on regeneration. 172 | 173 | [More information](http://www.airwindows.com/mv) 174 | 175 | ## Rasp 176 | 177 | De-edger, high frequency tamer, acceleration limiter 178 | 179 | ![Rackwindows Rasp](res/images/rasp_panels.jpg) 180 | 181 | Rasp combines Slew/Slew2/Slew3 and Acceleration in a single module for versatile high frequency conditioning. The Slew algorithms are significantly different in character and can be selected in the menu. 182 | 183 | **Routing:** If only the *Limit* output is connected, Slew is routed into Acceleration. If only the *Clamp* output is connected, Acceleration runs into Slew. If both outputs are connected, *Clamp* outputs Slew and *Limit* outputs Acceleration. 184 | 185 | [More information on Acceleration](https://www.airwindows.com/acceleration/) 186 | [Slew](https://www.airwindows.com/slew-2/), [Slew2](https://www.airwindows.com/slew2-2/), [Slew3](https://www.airwindows.com/slew3/) 187 | 188 | ## ResEQ 189 | 190 | Resonance equalizer 191 | 192 | ![Rackwindows ResEQ](res/images/reseq_panels.jpg) 193 | 194 | ResEQ passes audio through up to four adjustable frequency bands - and only those bands. All bands are similar and with identical range, but interact with each other in a particular way. They can be stacked for massive boosts or thin the signal out if set slightly apart. What sounds artificial in isolation, can be useful to highlight certain characteristics of a sound and then blend to taste with the dry/wet for more natural results. 195 | 196 | [More information](https://www.airwindows.com/reseq-vst/) 197 | 198 | ## Tape 199 | 200 | All-purpose tape mojo 201 | 202 | ![Rackwindows Tape](res/images/tape_panels.jpg) 203 | 204 | Tape is Airwindows' attempt to capture the very essence of what running audio through tape machines does to the signal. Driving it not only alters the volume, but also applies certain characteristics which are often associated with tape. The bump control allows to adjust the low end. 205 | 206 | [More information](http://www.airwindows.com/tape) 207 | 208 | ## Tremolo 209 | 210 | Organic pulsating thing 211 | 212 | ![Rackwindows Tremolo](res/images/tremolo_panels.jpg) 213 | 214 | Instead of volume animation Tremolo uses fluctuating saturation and antisaturation curves to create a more organic, somewhat tubey tremolo effect. 215 | 216 | [More information](http://www.airwindows.com/tremolo-vst) 217 | 218 | ## Vibrato 219 | 220 | FM Vibrator o_O 221 | 222 | ![Rackwindows Vibrato](res/images/vibrato_panels.jpg) 223 | 224 | Make sure to also play with the 'Inverse/Wet' knob for chorusing and flange effects. Two trigger outputs have been added for fun. 225 | 226 | [More information](http://www.airwindows.com/vibrato-vst) 227 | 228 | ## A word on processing quality 229 | 230 | Most modules feature an **Eco** mode in order to reduce CPU usage on weaker systems. The actual algorithms remain untouched, but any noise shaping/dithering is skipped. This can result in speed improvements of roughly 10% to 50% depending on the module. 231 | 232 | ## Building from Source 233 | 234 | To compile the modules from source, see the official [VCV Rack documentation](https://vcvrack.com/manual/Building.html). 235 | 236 | ## Colophon 237 | 238 | The typeface used on the panels is [Barlow](https://github.com/jpt/barlow) by Jeremy Tribby. -------------------------------------------------------------------------------- /src/tremolo.cpp: -------------------------------------------------------------------------------- 1 | 2 | /*********************************************************************************************** 3 | Tremolo 4 | ------- 5 | VCV Rack module based on Tremolo by Chris Johnson from Airwindows 6 | 7 | Ported and designed by Jens Robert Janke 8 | 9 | Changes/Additions: 10 | - CV inputs for speed and depth 11 | - polyphonic 12 | 13 | See ./LICENSE.md for all licenses 14 | ************************************************************************************************/ 15 | 16 | #include "plugin.hpp" 17 | 18 | // quality options 19 | #define ECO 0 20 | #define HIGH 1 21 | 22 | struct Tremolo : Module { 23 | enum ParamIds { 24 | SPEED_PARAM, 25 | DEPTH_PARAM, 26 | NUM_PARAMS 27 | }; 28 | enum InputIds { 29 | CLOCK_CV_INPUT, 30 | SPEED_CV_INPUT, 31 | DEPTH_CV_INPUT, 32 | IN_INPUT, 33 | NUM_INPUTS 34 | }; 35 | enum OutputIds { 36 | OUT_OUTPUT, 37 | NUM_OUTPUTS 38 | }; 39 | enum LightIds { 40 | SPEED_LIGHT, 41 | NUM_LIGHTS 42 | }; 43 | 44 | // module variables 45 | const double gainCut = 0.03125; 46 | const double gainBoost = 32.0; 47 | int quality; 48 | 49 | // control parameters 50 | float speedParam; 51 | float depthParam; 52 | 53 | // state variables (as arrays in order to handle up to 16 polyphonic channels) 54 | double sweep[16]; 55 | double speedChase[16]; 56 | double depthChase[16]; 57 | double speedAmount[16]; 58 | double depthAmount[16]; 59 | double lastSpeed[16]; 60 | double lastDepth[16]; 61 | long double fpNShape[16]; 62 | 63 | // other variables, which do not need to be updated every cycle 64 | double overallscale; 65 | double speedSpeed; 66 | double depthSpeed; 67 | float lastSpeedParam; 68 | float lastDepthParam; 69 | 70 | // constants 71 | const double tupi = 3.141592653589793238; 72 | 73 | Tremolo() 74 | { 75 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 76 | configParam(SPEED_PARAM, 0.f, 1.f, 0.f, "Speed"); 77 | configParam(DEPTH_PARAM, 0.f, 1.f, 0.f, "Depth"); 78 | 79 | quality = loadQuality(); 80 | onReset(); 81 | } 82 | 83 | void onReset() override 84 | { 85 | onSampleRateChange(); 86 | 87 | speedSpeed = 0.0; 88 | depthSpeed = 0.0; 89 | lastSpeedParam = 0.0; 90 | lastDepthParam = 0.0; 91 | 92 | for (int i = 0; i < 16; i++) { 93 | sweep[i] = 3.141592653589793238 / 2.0; 94 | speedChase[i] = 0.0; 95 | depthChase[i] = 0.0; 96 | speedAmount[i] = 1.0; 97 | depthAmount[i] = 0.0; 98 | lastSpeed[i] = 1000.0; 99 | lastDepth[i] = 1000.0; 100 | fpNShape[i] = 0.0; 101 | } 102 | } 103 | 104 | void onSampleRateChange() override 105 | { 106 | float sampleRate = APP->engine->getSampleRate(); 107 | 108 | overallscale = 1.0; 109 | overallscale /= 44100.0; 110 | overallscale *= sampleRate; 111 | } 112 | 113 | json_t* dataToJson() override 114 | { 115 | json_t* rootJ = json_object(); 116 | 117 | // quality 118 | json_object_set_new(rootJ, "quality", json_integer(quality)); 119 | 120 | return rootJ; 121 | } 122 | 123 | void dataFromJson(json_t* rootJ) override 124 | { 125 | // quality 126 | json_t* qualityJ = json_object_get(rootJ, "quality"); 127 | if (qualityJ) 128 | quality = json_integer_value(qualityJ); 129 | } 130 | 131 | void process(const ProcessArgs& args) override 132 | { 133 | if (outputs[OUT_OUTPUT].isConnected()) { 134 | 135 | speedParam = params[SPEED_PARAM].getValue(); 136 | speedParam += inputs[SPEED_CV_INPUT].getVoltage() / 5; 137 | speedParam = clamp(speedParam, 0.01f, 0.99f); 138 | 139 | depthParam = params[DEPTH_PARAM].getValue(); 140 | depthParam += inputs[DEPTH_CV_INPUT].getVoltage() / 5; 141 | depthParam = clamp(depthParam, 0.01f, 0.99f); 142 | 143 | double speed; 144 | double depth; 145 | double skew; 146 | double density; 147 | double control; 148 | double tempcontrol; 149 | double thickness; 150 | double out; 151 | double bridgerectifier; 152 | double offset; 153 | long double inputSample; 154 | long double drySample; 155 | 156 | // number of polyphonic channels 157 | int numChannels = std::max(1, inputs[IN_INPUT].getChannels()); 158 | 159 | // for each poly channel 160 | for (int i = 0; i < numChannels; i++) { 161 | 162 | if (speedParam != lastSpeedParam) { 163 | speedChase[i] = pow(speedParam, 4); 164 | speedSpeed = 300 / (fabs(lastSpeed[i] - speedChase[i]) + 1.0); 165 | lastSpeed[i] = speedChase[i]; 166 | } 167 | 168 | if (depthParam != lastDepthParam) { 169 | depthChase[i] = depthParam; 170 | depthSpeed = 300 / (fabs(lastDepth[i] - depthChase[i]) + 1.0); 171 | lastDepth[i] = depthChase[i]; 172 | } 173 | 174 | // input 175 | inputSample = inputs[IN_INPUT].getPolyVoltage(i); 176 | 177 | // pad gain 178 | inputSample *= gainCut; 179 | 180 | if (quality == HIGH) { 181 | if (inputSample < 1.2e-38 && -inputSample < 1.2e-38) { 182 | static int noisesource = 0; 183 | //this declares a variable before anything else is compiled. It won't keep assigning 184 | //it to 0 for every sample, it's as if the declaration doesn't exist in this context, 185 | //but it lets me add this denormalization fix in a single place rather than updating 186 | //it in three different locations. The variable isn't thread-safe but this is only 187 | //a random seed and we can share it with whatever. 188 | noisesource = noisesource % 1700021; 189 | noisesource++; 190 | int residue = noisesource * noisesource; 191 | residue = residue % 170003; 192 | residue *= residue; 193 | residue = residue % 17011; 194 | residue *= residue; 195 | residue = residue % 1709; 196 | residue *= residue; 197 | residue = residue % 173; 198 | residue *= residue; 199 | residue = residue % 17; 200 | double applyresidue = residue; 201 | applyresidue *= 0.00000001; 202 | applyresidue *= 0.00000001; 203 | inputSample = applyresidue; 204 | } 205 | } 206 | 207 | drySample = inputSample; 208 | 209 | speedAmount[i] = (((speedAmount[i] * speedSpeed) + speedChase[i]) / (speedSpeed + 1.0)); 210 | depthAmount[i] = (((depthAmount[i] * depthSpeed) + depthChase[i]) / (depthSpeed + 1.0)); 211 | speed = 0.0001 + (speedAmount[i] / 1000.0); 212 | speed /= overallscale; 213 | depth = 1.0 - pow(1.0 - depthAmount[i], 5); 214 | skew = 1.0 + pow(depthAmount[i], 9); 215 | density = ((1.0 - depthAmount[i]) * 2.0) - 1.0; 216 | 217 | offset = sin(sweep[i]); 218 | sweep[i] += speed; 219 | if (sweep[i] > tupi) { 220 | sweep[i] -= tupi; 221 | } 222 | control = fabs(offset); 223 | if (density > 0) { 224 | tempcontrol = sin(control); 225 | control = (control * (1.0 - density)) + (tempcontrol * density); 226 | } else { 227 | tempcontrol = 1 - cos(control); 228 | control = (control * (1.0 + density)) + (tempcontrol * -density); 229 | } 230 | //produce either boosted or starved version of control signal 231 | //will go from 0 to 1 232 | 233 | thickness = ((control * 2.0) - 1.0) * skew; 234 | out = fabs(thickness); 235 | 236 | //max value for sine function 237 | bridgerectifier = fabs(inputSample); 238 | if (bridgerectifier > 1.57079633) 239 | bridgerectifier = 1.57079633; 240 | 241 | //produce either boosted or starved version 242 | if (thickness > 0) 243 | bridgerectifier = sin(bridgerectifier); 244 | else 245 | bridgerectifier = 1 - cos(bridgerectifier); 246 | 247 | if (inputSample > 0) 248 | inputSample = (inputSample * (1 - out)) + (bridgerectifier * out); 249 | else 250 | inputSample = (inputSample * (1 - out)) - (bridgerectifier * out); 251 | 252 | //blend according to density control 253 | inputSample *= (1.0 - control); 254 | inputSample *= 2.0; 255 | //apply tremolo, apply gain boost to compensate for volume loss 256 | inputSample = (drySample * (1 - depth)) + (inputSample * depth); 257 | 258 | if (quality == HIGH) { 259 | //stereo 32 bit dither, made small and tidy. 260 | int expon; 261 | frexpf((float)inputSample, &expon); 262 | long double dither = (rand() / (RAND_MAX * 7.737125245533627e+25)) * pow(2, expon + 62); 263 | inputSample += (dither - fpNShape[i]); 264 | fpNShape[i] = dither; 265 | } 266 | 267 | // bring gain back up 268 | inputSample *= gainBoost; 269 | 270 | // output 271 | outputs[OUT_OUTPUT].setChannels(numChannels); 272 | outputs[OUT_OUTPUT].setVoltage(inputSample, i); 273 | } 274 | 275 | // lights 276 | lights[SPEED_LIGHT].setSmoothBrightness(fmaxf(0.0, (-sweep[0]) + 1), args.sampleTime); 277 | } 278 | } 279 | }; 280 | 281 | struct TremoloWidget : ModuleWidget { 282 | 283 | // quality item 284 | struct QualityItem : MenuItem { 285 | Tremolo* module; 286 | int quality; 287 | 288 | void onAction(const event::Action& e) override 289 | { 290 | module->quality = quality; 291 | } 292 | 293 | void step() override 294 | { 295 | rightText = (module->quality == quality) ? "✔" : ""; 296 | } 297 | }; 298 | 299 | void appendContextMenu(Menu* menu) override 300 | { 301 | Tremolo* module = dynamic_cast(this->module); 302 | assert(module); 303 | 304 | menu->addChild(new MenuSeparator()); // separator 305 | 306 | MenuLabel* qualityLabel = new MenuLabel(); // menu label 307 | qualityLabel->text = "Quality"; 308 | menu->addChild(qualityLabel); 309 | 310 | QualityItem* low = new QualityItem(); // low quality 311 | low->text = "Eco"; 312 | low->module = module; 313 | low->quality = 0; 314 | menu->addChild(low); 315 | 316 | QualityItem* high = new QualityItem(); // high quality 317 | high->text = "High"; 318 | high->module = module; 319 | high->quality = 1; 320 | menu->addChild(high); 321 | } 322 | 323 | TremoloWidget(Tremolo* module) 324 | { 325 | setModule(module); 326 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/tremolo_dark.svg"))); 327 | 328 | // screws 329 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 1.5, 0))); 330 | addChild(createWidget(Vec(RACK_GRID_WIDTH * 1.5, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 331 | 332 | // knobs 333 | addParam(createParamCentered(Vec(30.0, 65.0), module, Tremolo::SPEED_PARAM)); 334 | addParam(createParamCentered(Vec(30.0, 120.0), module, Tremolo::DEPTH_PARAM)); 335 | 336 | // lights 337 | addChild(createLightCentered>(Vec(11, 103), module, Tremolo::SPEED_LIGHT)); 338 | 339 | // inputs 340 | // addInput(createInputCentered(Vec(30.0, 165.0), module, Tremolo::CLOCK_CV_INPUT)); 341 | addInput(createInputCentered(Vec(30.0, 205.0), module, Tremolo::SPEED_CV_INPUT)); 342 | addInput(createInputCentered(Vec(30.0, 245.0), module, Tremolo::DEPTH_CV_INPUT)); 343 | addInput(createInputCentered(Vec(30.0, 285.0), module, Tremolo::IN_INPUT)); 344 | 345 | // outputs 346 | addOutput(createOutputCentered(Vec(30.0, 325.0), module, Tremolo::OUT_OUTPUT)); 347 | } 348 | }; 349 | 350 | Model* modelTremolo = createModel("tremolo"); -------------------------------------------------------------------------------- /src/holt.cpp: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | Holt 3 | ------ 4 | VCV Rack module based on Holt by Chris Johnson from Airwindows 5 | 6 | Ported and designed by Jens Robert Janke 7 | 8 | Changes/Additions: 9 | - mono 10 | - CV inputs for frequency, resonance and poles 11 | - no output control 12 | - no dry/wet 13 | - polyphonic 14 | - added output saturation 15 | 16 | See ./LICENSE.md for all licenses 17 | ************************************************************************************************/ 18 | 19 | /* 20 | Note: for the sake of porting variety, this one encapsulates the entire audio plugin as its own entity 21 | Advantages: cleaner module logic, way easier and less messy handling of polyphony 22 | Drawbacks: possibly sliiightly less speedy 23 | */ 24 | 25 | #include "plugin.hpp" 26 | 27 | // quality options 28 | #define ECO 0 29 | #define HIGH 1 30 | 31 | /* Engine (the audio plugin code, single channel) 32 | ======================================================================================== */ 33 | struct HoltEngine { 34 | 35 | long double previousSampleA; 36 | long double previousTrendA; 37 | long double previousSampleB; 38 | long double previousTrendB; 39 | long double previousSampleC; 40 | long double previousTrendC; 41 | long double previousSampleD; 42 | long double previousTrendD; 43 | 44 | double alpha; 45 | double beta; 46 | float lastFrequencyParam; 47 | float lastResonanceParam; 48 | 49 | HoltEngine() 50 | { 51 | previousSampleA = 0.0; 52 | previousTrendA = 0.0; 53 | previousSampleB = 0.0; 54 | previousTrendB = 0.0; 55 | previousSampleC = 0.0; 56 | previousTrendC = 0.0; 57 | previousSampleD = 0.0; 58 | previousTrendD = 0.0; 59 | 60 | alpha = 0.0; 61 | beta = 0.0; 62 | lastFrequencyParam = 0.0f; 63 | lastResonanceParam = 0.0f; 64 | } 65 | 66 | long double process(long double inputSample, float frequencyParam = 1.0, float resonanceParam = 0.0, float polesParam = 1.0, float outputParam = 1.0, float drywetParam = 1.0) 67 | { 68 | if ((frequencyParam != lastFrequencyParam) || (resonanceParam != lastResonanceParam)) { 69 | alpha = pow(frequencyParam, 4) + 0.00001; 70 | if (alpha > 1.0) { 71 | alpha = 1.0; 72 | } 73 | 74 | beta = (alpha * pow(resonanceParam, 2)) + 0.00001; 75 | alpha += ((1.0 - beta) * pow(frequencyParam, 3)); //correct for droop in frequency 76 | if (alpha > 1.0) { 77 | alpha = 1.0; 78 | } 79 | 80 | lastFrequencyParam = frequencyParam; 81 | lastResonanceParam = resonanceParam; 82 | } 83 | 84 | long double trend; 85 | long double forecast; //defining these here because we're copying the routine four times 86 | 87 | //four-stage wet/dry control using progressive stages that bypass when not engaged 88 | double aWet = 1.0; 89 | double bWet = 1.0; 90 | double cWet = 1.0; 91 | double dWet = polesParam * 4.0; 92 | 93 | if (dWet < 1.0) { 94 | aWet = dWet; 95 | bWet = 0.0; 96 | cWet = 0.0; 97 | dWet = 0.0; 98 | } else if (dWet < 2.0) { 99 | bWet = dWet - 1.0; 100 | cWet = 0.0; 101 | dWet = 0.0; 102 | } else if (dWet < 3.0) { 103 | cWet = dWet - 2.0; 104 | dWet = 0.0; 105 | } else { 106 | dWet -= 3.0; 107 | } 108 | //this is one way to make a little set of dry/wet stages that are successively added to the 109 | //output as the control is turned up. Each one independently goes from 0-1 and stays at 1 110 | //beyond that point: this is a way to progressively add a 'black box' sound processing 111 | //which lets you fall through to simpler processing at lower settings. 112 | 113 | double gain = outputParam; 114 | double wet = drywetParam; 115 | 116 | long double drySample = inputSample; 117 | 118 | if (aWet > 0.0) { 119 | trend = (beta * (inputSample - previousSampleA) + ((0.999 - beta) * previousTrendA)); 120 | forecast = previousSampleA + previousTrendA; 121 | inputSample = (alpha * inputSample) + ((0.999 - alpha) * forecast); 122 | previousSampleA = inputSample; 123 | previousTrendA = trend; 124 | inputSample = (inputSample * aWet) + (drySample * (1.0 - aWet)); 125 | } 126 | 127 | if (bWet > 0.0) { 128 | trend = (beta * (inputSample - previousSampleB) + ((0.999 - beta) * previousTrendB)); 129 | forecast = previousSampleB + previousTrendB; 130 | inputSample = (alpha * inputSample) + ((0.999 - alpha) * forecast); 131 | previousSampleB = inputSample; 132 | previousTrendB = trend; 133 | inputSample = (inputSample * bWet) + (previousSampleA * (1.0 - bWet)); 134 | } 135 | 136 | if (cWet > 0.0) { 137 | trend = (beta * (inputSample - previousSampleC) + ((0.999 - beta) * previousTrendC)); 138 | forecast = previousSampleC + previousTrendC; 139 | inputSample = (alpha * inputSample) + ((0.999 - alpha) * forecast); 140 | previousSampleC = inputSample; 141 | previousTrendC = trend; 142 | inputSample = (inputSample * cWet) + (previousSampleB * (1.0 - cWet)); 143 | } 144 | 145 | if (dWet > 0.0) { 146 | trend = (beta * (inputSample - previousSampleD) + ((0.999 - beta) * previousTrendD)); 147 | forecast = previousSampleD + previousTrendD; 148 | inputSample = (alpha * inputSample) + ((0.999 - alpha) * forecast); 149 | previousSampleD = inputSample; 150 | previousTrendD = trend; 151 | inputSample = (inputSample * dWet) + (previousSampleC * (1.0 - dWet)); 152 | } 153 | 154 | if (gain < 1.0) { 155 | inputSample *= gain; 156 | } 157 | 158 | //clip to 1.2533141373155 to reach maximum output 159 | if (inputSample > 1.2533141373155) 160 | inputSample = 1.2533141373155; 161 | if (inputSample < -1.2533141373155) 162 | inputSample = -1.2533141373155; 163 | inputSample = sin(inputSample * fabs(inputSample)) / ((inputSample == 0.0) ? 1 : fabs(inputSample)); 164 | 165 | if (wet < 1.0) { 166 | inputSample = (inputSample * wet) + (drySample * (1.0 - wet)); 167 | } 168 | 169 | return inputSample; 170 | } 171 | }; 172 | 173 | /* Dither Noise 174 | ======================================================================================== */ 175 | inline long double ditherNoise(long double in) 176 | { 177 | //for live air, we always apply the dither noise. Then, if our result is 178 | //effectively digital black, we'll subtract it again. We want a 'air' hiss 179 | 180 | static int noisesource = 0; 181 | int residue; 182 | double applyresidue; 183 | 184 | noisesource = noisesource % 1700021; 185 | noisesource++; 186 | residue = noisesource * noisesource; 187 | residue = residue % 170003; 188 | residue *= residue; 189 | residue = residue % 17011; 190 | residue *= residue; 191 | residue = residue % 1709; 192 | residue *= residue; 193 | residue = residue % 173; 194 | residue *= residue; 195 | residue = residue % 17; 196 | applyresidue = residue; 197 | applyresidue *= 0.00000001; 198 | applyresidue *= 0.00000001; 199 | in += applyresidue; 200 | if (in < 1.2e-38 && -in < 1.2e-38) { 201 | in -= applyresidue; 202 | } 203 | 204 | return in; 205 | } 206 | 207 | /* Mojo (for output saturation) 208 | ======================================================================================== */ 209 | inline long double mojo(long double in) 210 | { 211 | long double mojo = pow(fabs(in), 0.25); 212 | if (mojo > 0.0) { 213 | in = (sin(in * mojo * M_PI * 0.5) / mojo) * 0.987654321; 214 | in *= 0.65; // dial back a bit to keep levels roughly the same 215 | } 216 | return in; 217 | } 218 | 219 | /* Module 220 | ======================================================================================== */ 221 | struct Holt : Module { 222 | enum ParamIds { 223 | FREQUENCY_PARAM, 224 | RESONANCE_PARAM, 225 | POLES_PARAM, 226 | NUM_PARAMS 227 | }; 228 | enum InputIds { 229 | FREQUENCY_CV_INPUT, 230 | RESONANCE_CV_INPUT, 231 | POLES_CV_INPUT, 232 | IN_INPUT, 233 | NUM_INPUTS 234 | }; 235 | enum OutputIds { 236 | OUT_OUTPUT, 237 | NUM_OUTPUTS 238 | }; 239 | enum LightIds { 240 | NUM_LIGHTS 241 | }; 242 | 243 | // module variables 244 | const double gainCut = 0.03125; 245 | const double gainBoost = 32.0; 246 | int quality; 247 | HoltEngine holt[16]; 248 | 249 | // control parameter 250 | float frequencyParam; 251 | float resonanceParam; 252 | float polesParam; 253 | 254 | // other 255 | double overallscale; 256 | long double fpNShape; 257 | 258 | Holt() 259 | { 260 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 261 | configParam(FREQUENCY_PARAM, 0.f, 1.f, 1.f, "Frequency"); 262 | configParam(RESONANCE_PARAM, 0.f, 1.f, 0.f, "Resonance"); 263 | configParam(POLES_PARAM, 0.f, 1.f, 1.f, "Poles"); 264 | 265 | quality = loadQuality(); 266 | } 267 | 268 | void onSampleRateChange() override 269 | { 270 | float sampleRate = APP->engine->getSampleRate(); 271 | 272 | overallscale = 1.0; 273 | overallscale /= 44100.0; 274 | overallscale *= sampleRate; 275 | } 276 | 277 | void onReset() override 278 | { 279 | for (int i = 0; i < 16; i++) { 280 | holt[i] = HoltEngine(); 281 | } 282 | 283 | fpNShape = 0.0; 284 | 285 | onSampleRateChange(); 286 | updateParams(); 287 | } 288 | 289 | json_t* dataToJson() override 290 | { 291 | json_t* rootJ = json_object(); 292 | 293 | // quality 294 | json_object_set_new(rootJ, "quality", json_integer(quality)); 295 | 296 | return rootJ; 297 | } 298 | 299 | void dataFromJson(json_t* rootJ) override 300 | { 301 | // quality 302 | json_t* qualityJ = json_object_get(rootJ, "quality"); 303 | if (qualityJ) 304 | quality = json_integer_value(qualityJ); 305 | } 306 | 307 | void updateParams() 308 | { 309 | frequencyParam = params[FREQUENCY_PARAM].getValue(); 310 | frequencyParam += inputs[FREQUENCY_CV_INPUT].getVoltage() / 9; 311 | frequencyParam = clamp(frequencyParam, 0.01f, 0.99f); 312 | 313 | resonanceParam = params[RESONANCE_PARAM].getValue(); 314 | resonanceParam += inputs[RESONANCE_CV_INPUT].getVoltage() / 9; 315 | resonanceParam = clamp(resonanceParam, 0.01f, 0.99f); 316 | 317 | polesParam = params[POLES_PARAM].getValue(); 318 | polesParam += inputs[POLES_CV_INPUT].getVoltage() / 10; 319 | polesParam = clamp(polesParam, 0.01f, 0.99f); 320 | } 321 | 322 | void process(const ProcessArgs& args) override 323 | { 324 | updateParams(); 325 | 326 | long double in; 327 | 328 | // for each poly channel 329 | for (int i = 0, numChannels = std::max(1, inputs[IN_INPUT].getChannels()); i < numChannels; ++i) { 330 | 331 | // input 332 | in = inputs[IN_INPUT].getPolyVoltage(i) * gainCut; 333 | 334 | if (quality == HIGH) { 335 | in = ditherNoise(in); 336 | } 337 | 338 | // holt 339 | in = holt[i].process(in, frequencyParam, resonanceParam, polesParam); 340 | 341 | // mojo for swallowing excessive resonance 342 | in = mojo(in); 343 | 344 | if (quality == HIGH) { 345 | //stereo 32 bit dither, made small and tidy. 346 | int expon; 347 | frexpf((float)in, &expon); 348 | long double dither = (rand() / (RAND_MAX * 7.737125245533627e+25)) * pow(2, expon + 62); 349 | in += (dither - fpNShape); 350 | fpNShape = dither; 351 | } 352 | 353 | // output 354 | outputs[OUT_OUTPUT].setChannels(numChannels); 355 | outputs[OUT_OUTPUT].setVoltage(in * gainBoost, i); 356 | } 357 | } 358 | }; 359 | 360 | /* Widget 361 | ======================================================================================== */ 362 | struct HoltWidget : ModuleWidget { 363 | 364 | // quality item 365 | struct QualityItem : MenuItem { 366 | Holt* module; 367 | int quality; 368 | 369 | void onAction(const event::Action& e) override 370 | { 371 | module->quality = quality; 372 | } 373 | 374 | void step() override 375 | { 376 | rightText = (module->quality == quality) ? "✔" : ""; 377 | } 378 | }; 379 | 380 | void appendContextMenu(Menu* menu) override 381 | { 382 | Holt* module = dynamic_cast(this->module); 383 | assert(module); 384 | 385 | menu->addChild(new MenuSeparator()); // separator 386 | 387 | MenuLabel* qualityLabel = new MenuLabel(); // menu label 388 | qualityLabel->text = "Quality"; 389 | menu->addChild(qualityLabel); 390 | 391 | QualityItem* low = new QualityItem(); // low quality 392 | low->text = "Eco"; 393 | low->module = module; 394 | low->quality = 0; 395 | menu->addChild(low); 396 | 397 | QualityItem* high = new QualityItem(); // high quality 398 | high->text = "High"; 399 | high->module = module; 400 | high->quality = 1; 401 | menu->addChild(high); 402 | } 403 | 404 | HoltWidget(Holt* module) 405 | { 406 | setModule(module); 407 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/holt_dark.svg"))); 408 | 409 | // screws 410 | addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); 411 | addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); 412 | addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 413 | addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 414 | 415 | // params 416 | addParam(createParamCentered(Vec(45.0, 65.0), module, Holt::FREQUENCY_PARAM)); 417 | addParam(createParamCentered(Vec(45.0, 125.0), module, Holt::RESONANCE_PARAM)); 418 | addParam(createParamCentered(Vec(45.0, 185.0), module, Holt::POLES_PARAM)); 419 | 420 | // inputs 421 | addInput(createInputCentered(Vec(26.25, 245.0), module, Holt::FREQUENCY_CV_INPUT)); 422 | addInput(createInputCentered(Vec(63.75, 245.0), module, Holt::RESONANCE_CV_INPUT)); 423 | addInput(createInputCentered(Vec(45.0, 285.0), module, Holt::POLES_CV_INPUT)); 424 | addInput(createInputCentered(Vec(26.25, 325.0), module, Holt::IN_INPUT)); 425 | 426 | // output 427 | addOutput(createOutputCentered(Vec(63.75, 325.0), module, Holt::OUT_OUTPUT)); 428 | } 429 | }; 430 | 431 | Model* modelHolt = createModel("holt"); -------------------------------------------------------------------------------- /src/vibrato.cpp: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | Vibrato 3 | ------- 4 | VCV Rack module based on Vibrato by Chris Johnson from Airwindows 5 | 6 | Ported and designed by Jens Robert Janke 7 | 8 | Changes/Additions: 9 | - CV inputs for speed, depth, fmspeed, fmdepth and inv/wet 10 | - trigger outputs (EOC) for speed and fmspeed 11 | - polyphonic 12 | 13 | See ./LICENSE.md for all licenses 14 | ************************************************************************************************/ 15 | 16 | #include "plugin.hpp" 17 | 18 | // quality options 19 | #define ECO 0 20 | #define HIGH 1 21 | 22 | struct Vibrato : Module { 23 | 24 | enum ParamIds { 25 | SPEED_PARAM, 26 | FMSPEED_PARAM, 27 | DEPTH_PARAM, 28 | FMDEPTH_PARAM, 29 | INVWET_PARAM, 30 | NUM_PARAMS 31 | }; 32 | enum InputIds { 33 | SPEED_CV_INPUT, 34 | DEPTH_CV_INPUT, 35 | FMSPEED_CV_INPUT, 36 | FMDEPTH_CV_INPUT, 37 | INVWET_CV_INPUT, 38 | IN_INPUT, 39 | NUM_INPUTS 40 | }; 41 | enum OutputIds { 42 | EOC_OUTPUT, 43 | OUT_OUTPUT, 44 | EOC_FM_OUTPUT, 45 | NUM_OUTPUTS 46 | }; 47 | enum LightIds { 48 | SPEED_LIGHT, 49 | SPEED_FM_LIGHT, 50 | NUM_LIGHTS 51 | }; 52 | 53 | // module variables 54 | const double gainCut = 0.03125; 55 | const double gainBoost = 32.0; 56 | int quality; 57 | dsp::PulseGenerator eocPulse, eocFmPulse; 58 | 59 | // control parameters 60 | float speedParam; 61 | float depthParam; 62 | float fmSpeedParam; 63 | float fmDepthParam; 64 | float invwetParam; 65 | 66 | // state variables (as arrays in order to handle up to 16 polyphonic channels) 67 | double p[16][16386]; //this is processed, not raw incoming samples 68 | double sweep[16]; 69 | double sweepB[16]; 70 | int gcount[16]; 71 | double airPrev[16]; 72 | double airEven[16]; 73 | double airOdd[16]; 74 | double airFactor[16]; 75 | bool flip[16]; 76 | uint32_t fpd[16]; 77 | 78 | // other variables, which do not need to be updated every cycle 79 | double overallscale; 80 | double speed; 81 | double depth; 82 | double speedB; 83 | double depthB; 84 | double wet; 85 | float lastSpeedParam; 86 | float lastDepthParam; 87 | float lastFmSpeedParam; 88 | float lastFmDepthParam; 89 | float lastInvwetParam; 90 | 91 | // constants 92 | const double tupi = 3.141592653589793238 * 2.0; 93 | 94 | Vibrato() 95 | { 96 | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); 97 | configParam(SPEED_PARAM, 0.f, 1.f, 0.f, "Speed"); 98 | configParam(FMSPEED_PARAM, 0.f, 1.f, 0.f, "FM Speed"); 99 | configParam(DEPTH_PARAM, 0.f, 1.f, 0.f, "Depth"); 100 | configParam(FMDEPTH_PARAM, 0.f, 1.f, 0.f, "FM Depth"); 101 | configParam(INVWET_PARAM, 0.f, 1.f, 0.5f, "Inv/Wet"); 102 | 103 | quality = loadQuality(); 104 | onReset(); 105 | } 106 | 107 | void onReset() override 108 | { 109 | onSampleRateChange(); 110 | 111 | lastSpeedParam = 0.0; 112 | lastDepthParam = 0.0; 113 | lastFmSpeedParam = 0.0; 114 | lastFmDepthParam = 0.0; 115 | lastInvwetParam = 0.0; 116 | 117 | for (int i = 0; i < 16; i++) { 118 | for (int count = 0; count < 16385; count++) { 119 | p[i][count] = 0.0; 120 | } 121 | sweep[i] = sweepB[i] = 3.141592653589793238 / 2.0; 122 | gcount[i] = 0; 123 | 124 | airPrev[i] = 0.0; 125 | airEven[i] = 0.0; 126 | airOdd[i] = 0.0; 127 | airFactor[i] = 0.0; 128 | 129 | flip[i] = false; 130 | 131 | fpd[i] = 17; 132 | } 133 | } 134 | 135 | void onSampleRateChange() override 136 | { 137 | float sampleRate = APP->engine->getSampleRate(); 138 | 139 | overallscale = 1.0; 140 | overallscale /= 44100.0; 141 | overallscale *= sampleRate; 142 | } 143 | 144 | json_t* dataToJson() override 145 | { 146 | json_t* rootJ = json_object(); 147 | 148 | // quality 149 | json_object_set_new(rootJ, "quality", json_integer(quality)); 150 | 151 | return rootJ; 152 | } 153 | 154 | void dataFromJson(json_t* rootJ) override 155 | { 156 | // quality 157 | json_t* qualityJ = json_object_get(rootJ, "quality"); 158 | if (qualityJ) 159 | quality = json_integer_value(qualityJ); 160 | } 161 | 162 | void process(const ProcessArgs& args) override 163 | { 164 | if (outputs[OUT_OUTPUT].isConnected() || outputs[EOC_OUTPUT].isConnected() || outputs[EOC_FM_OUTPUT].isConnected()) { 165 | 166 | speedParam = params[SPEED_PARAM].getValue(); 167 | speedParam += inputs[SPEED_CV_INPUT].getVoltage() / 5; 168 | speedParam = clamp(speedParam, 0.01f, 0.99f); 169 | 170 | depthParam = params[DEPTH_PARAM].getValue(); 171 | depthParam += inputs[DEPTH_CV_INPUT].getVoltage() / 5; 172 | depthParam = clamp(depthParam, 0.01f, 0.99f); 173 | 174 | fmSpeedParam = params[FMSPEED_PARAM].getValue(); 175 | fmSpeedParam += inputs[FMSPEED_CV_INPUT].getVoltage() / 5; 176 | fmSpeedParam = clamp(fmSpeedParam, 0.01f, 0.99f); 177 | 178 | fmDepthParam = params[FMDEPTH_PARAM].getValue(); 179 | fmDepthParam += inputs[FMDEPTH_CV_INPUT].getVoltage() / 5; 180 | fmDepthParam = clamp(fmDepthParam, 0.01f, 0.99f); 181 | 182 | invwetParam = params[INVWET_PARAM].getValue(); 183 | invwetParam += inputs[INVWET_CV_INPUT].getVoltage() / 5; 184 | invwetParam = clamp(invwetParam, 0.01f, 0.99f); 185 | 186 | if (speedParam != lastSpeedParam) { 187 | speed = pow(0.1 + speedParam, 6); 188 | } 189 | 190 | if (depthParam != lastDepthParam) { 191 | depth = (pow(depthParam, 3) / sqrt(speed)) * 4.0; 192 | } 193 | 194 | if (fmSpeedParam != lastFmSpeedParam) { 195 | speedB = pow(0.1 + fmSpeedParam, 6); 196 | } 197 | 198 | if (fmDepthParam != lastFmDepthParam) { 199 | depthB = pow(fmDepthParam, 3) / sqrt(speedB); 200 | } 201 | 202 | if (invwetParam != lastInvwetParam) { 203 | wet = (invwetParam * 2.0) - 1.0; //note: inv/dry/wet 204 | } 205 | 206 | // number of polyphonic channels 207 | int numChannels = std::max(1, inputs[IN_INPUT].getChannels()); 208 | 209 | // for each poly channel 210 | for (int i = 0; i < numChannels; i++) { 211 | 212 | // input 213 | long double inputSample = inputs[IN_INPUT].getPolyVoltage(i); 214 | 215 | // pad gain 216 | inputSample *= gainCut; 217 | 218 | if (quality == HIGH) { 219 | if (fabs(inputSample) < 1.18e-37) 220 | inputSample = fpd[i] * 1.18e-37; 221 | } 222 | 223 | double drySample = inputSample; 224 | 225 | airFactor[i] = airPrev[i] - inputSample; 226 | 227 | if (flip[i]) { 228 | airEven[i] += airFactor[i]; 229 | airOdd[i] -= airFactor[i]; 230 | airFactor[i] = airEven[i]; 231 | } else { 232 | airOdd[i] += airFactor[i]; 233 | airEven[i] -= airFactor[i]; 234 | airFactor[i] = airOdd[i]; 235 | } 236 | 237 | //air, compensates for loss of highs in the interpolation 238 | airOdd[i] = (airOdd[i] - ((airOdd[i] - airEven[i]) / 256.0)) / 1.0001; 239 | airEven[i] = (airEven[i] - ((airEven[i] - airOdd[i]) / 256.0)) / 1.0001; 240 | airPrev[i] = inputSample; 241 | inputSample += airFactor[i]; 242 | 243 | flip[i] = !flip[i]; 244 | 245 | if (gcount[i] < 1 || gcount[i] > 8192) { 246 | gcount[i] = 8192; 247 | } 248 | int count = gcount[i]; 249 | p[i][count + 8192] = p[i][count] = inputSample; 250 | 251 | double offset = depth + (depth * sin(sweep[i])); 252 | count += (int)floor(offset); 253 | 254 | inputSample = p[i][count] * (1.0 - (offset - floor(offset))); //less as value moves away from .0 255 | inputSample += p[i][count + 1]; //we can assume always using this in one way or another? 256 | inputSample += p[i][count + 2] * (offset - floor(offset)); //greater as value moves away from .0 257 | inputSample -= ((p[i][count] - p[i][count + 1]) - (p[i][count + 1] - p[i][count + 2])) / 50.0; //interpolation hacks 'r us 258 | inputSample *= 0.5; // gain trim 259 | 260 | //still scrolling through the samples, remember 261 | sweep[i] += (speed + (speedB * sin(sweepB[i]) * depthB)); 262 | sweepB[i] += speedB; 263 | if (sweep[i] > tupi) { 264 | sweep[i] -= tupi; 265 | } 266 | if (sweep[i] < 0.0) { 267 | sweep[i] += tupi; 268 | } //through zero FM 269 | if (sweepB[i] > tupi) { 270 | sweepB[i] -= tupi; 271 | } 272 | gcount[i]--; 273 | 274 | //Inv/Dry/Wet control 275 | if (wet != 1.0) { 276 | inputSample = (inputSample * wet) + (drySample * (1.0 - fabs(wet))); 277 | } 278 | 279 | if (quality == HIGH) { 280 | //begin 32 bit stereo floating point dither 281 | int expon; 282 | frexpf((float)inputSample, &expon); 283 | fpd[i] ^= fpd[i] << 13; 284 | fpd[i] ^= fpd[i] >> 17; 285 | fpd[i] ^= fpd[i] << 5; 286 | inputSample += ((double(fpd[i]) - uint32_t(0x7fffffff)) * 5.5e-36l * pow(2, expon + 62)); 287 | //end 32 bit stereo floating point dither 288 | } 289 | 290 | // bring gain back up 291 | inputSample *= gainBoost; 292 | 293 | // audio output 294 | outputs[OUT_OUTPUT].setChannels(numChannels); 295 | outputs[OUT_OUTPUT].setVoltage(inputSample, i); 296 | } 297 | 298 | // triggers 299 | if (sweep[0] < 0.1) { 300 | eocPulse.trigger(1e-3); 301 | } 302 | if (sweepB[0] < 0.1) { 303 | eocFmPulse.trigger(1e-3); 304 | } 305 | 306 | // lights 307 | lights[SPEED_LIGHT].setSmoothBrightness(fmaxf(0.0, (-sweep[0] / 5) + 1), args.sampleTime); 308 | lights[SPEED_FM_LIGHT].setSmoothBrightness(fmaxf(0.0, (-sweepB[0] / 5) + 1), args.sampleTime); 309 | 310 | // trigger outputs 311 | outputs[EOC_OUTPUT].setVoltage((eocPulse.process(args.sampleTime) ? 10.0 : 0.0)); 312 | outputs[EOC_FM_OUTPUT].setVoltage((eocFmPulse.process(args.sampleTime) ? 10.0 : 0.0)); 313 | } 314 | } 315 | }; 316 | 317 | struct VibratoWidget : ModuleWidget { 318 | 319 | // quality item 320 | struct QualityItem : MenuItem { 321 | Vibrato* module; 322 | int quality; 323 | 324 | void onAction(const event::Action& e) override 325 | { 326 | module->quality = quality; 327 | } 328 | 329 | void step() override 330 | { 331 | rightText = (module->quality == quality) ? "✔" : ""; 332 | } 333 | }; 334 | 335 | void appendContextMenu(Menu* menu) override 336 | { 337 | Vibrato* module = dynamic_cast(this->module); 338 | assert(module); 339 | 340 | menu->addChild(new MenuSeparator()); // separator 341 | 342 | MenuLabel* qualityLabel = new MenuLabel(); // menu label 343 | qualityLabel->text = "Quality"; 344 | menu->addChild(qualityLabel); 345 | 346 | QualityItem* low = new QualityItem(); // low quality 347 | low->text = "Eco"; 348 | low->module = module; 349 | low->quality = 0; 350 | menu->addChild(low); 351 | 352 | QualityItem* high = new QualityItem(); // high quality 353 | high->text = "High"; 354 | high->module = module; 355 | high->quality = 1; 356 | menu->addChild(high); 357 | } 358 | 359 | VibratoWidget(Vibrato* module) 360 | { 361 | setModule(module); 362 | setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/vibrato_dark.svg"))); 363 | 364 | // screws 365 | addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); 366 | addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); 367 | addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 368 | addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); 369 | 370 | // knobs 371 | addParam(createParamCentered(Vec(30.0, 65.0), module, Vibrato::SPEED_PARAM)); 372 | addParam(createParamCentered(Vec(90.0, 65.0), module, Vibrato::FMSPEED_PARAM)); 373 | addParam(createParamCentered(Vec(30.0, 125.0), module, Vibrato::DEPTH_PARAM)); 374 | addParam(createParamCentered(Vec(90.0, 125.0), module, Vibrato::FMDEPTH_PARAM)); 375 | addParam(createParamCentered(Vec(60.0, 190.0), module, Vibrato::INVWET_PARAM)); 376 | 377 | // lights 378 | addChild(createLightCentered>(Vec(13, 37), module, Vibrato::SPEED_LIGHT)); 379 | addChild(createLightCentered>(Vec(107, 37), module, Vibrato::SPEED_FM_LIGHT)); 380 | 381 | // inputs 382 | addInput(createInputCentered(Vec(22.5, 245.0), module, Vibrato::SPEED_CV_INPUT)); 383 | addInput(createInputCentered(Vec(22.5, 285.0), module, Vibrato::DEPTH_CV_INPUT)); 384 | addInput(createInputCentered(Vec(97.5, 245.0), module, Vibrato::FMSPEED_CV_INPUT)); 385 | addInput(createInputCentered(Vec(97.5, 285.0), module, Vibrato::FMDEPTH_CV_INPUT)); 386 | addInput(createInputCentered(Vec(60.0, 245.0), module, Vibrato::INVWET_CV_INPUT)); 387 | addInput(createInputCentered(Vec(60.0, 285.0), module, Vibrato::IN_INPUT)); 388 | 389 | // outputs 390 | addOutput(createOutputCentered(Vec(22.5, 325.0), module, Vibrato::EOC_OUTPUT)); 391 | addOutput(createOutputCentered(Vec(60.0, 325.0), module, Vibrato::OUT_OUTPUT)); 392 | addOutput(createOutputCentered(Vec(97.5, 325.0), module, Vibrato::EOC_FM_OUTPUT)); 393 | } 394 | }; 395 | 396 | Model* modelVibrato = createModel("vibrato"); --------------------------------------------------------------------------------