├── .github └── workflows │ ├── beta.yml │ ├── ci.yml │ └── codecov.yml ├── .gitignore ├── LICENSE ├── README.md ├── dscanner.ini ├── dub.json ├── dub.selections.json ├── plugin.json ├── resource ├── 114.png ├── 65.png ├── 83.png ├── FORCED SQUARE.ttf ├── LeroyLetteringLightBeta01.ttf ├── TFB.ttf ├── black.png ├── filter_coeff.d ├── gray.png ├── module-au.lst ├── module-lv2.lst ├── module-lv2.ver ├── module-vst.lst ├── module-vst.ver ├── module-vst3.lst ├── module-vst3.ver ├── screen.png └── white.png ├── source └── synth2 │ ├── chorus.d │ ├── client.d │ ├── delay.d │ ├── effect.d │ ├── envelope.d │ ├── equalizer.d │ ├── filter.d │ ├── gui.d │ ├── lfo.d │ ├── modfilter.d │ ├── oscillator.d │ ├── params.d │ ├── random.d │ ├── ringbuffer.d │ ├── voice.d │ └── waveform.d └── tool └── filter_coeff.py /.github/workflows/beta.yml: -------------------------------------------------------------------------------- 1 | name: beta 2 | 3 | on: 4 | push: 5 | # Nightly builds 6 | schedule: 7 | - cron: '00 00 * * *' 8 | 9 | # Common variables for all platforms (ldc is hardcoded in windows job) 10 | env: 11 | VST2_SDK: ${{ github.workspace }}/VST2_SDK 12 | SETUP_VST2_SDK: true 13 | # List of commands 14 | DPlugBuild: ${{ github.workspace }}/Dplug/tools/dplug-build/dplug-build 15 | DplugProcess: ${{ github.workspace }}/Dplug/tools/process/process 16 | 17 | 18 | defaults: 19 | run: 20 | shell: pwsh 21 | 22 | jobs: 23 | Test: 24 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 25 | 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | os: 31 | - windows-latest 32 | - ubuntu-latest 33 | - macOS-latest 34 | compiler: 35 | - 'ldc-beta' 36 | 37 | steps: 38 | # Checkout 39 | - name: Checkout master branch 40 | uses: actions/checkout@v2 41 | with: 42 | fetch-depth: 0 43 | 44 | - name: Checkout Dplug repo 45 | uses: actions/checkout@v2 46 | with: 47 | repository: AuburnSounds/Dplug 48 | path: Dplug 49 | 50 | # Cache 51 | - name: Cache 52 | id: synth2-cache 53 | uses: actions/cache@v2 54 | env: 55 | cache-name: synth2-cache 56 | with: 57 | path: | 58 | ${{ env.VST2_SDK }} 59 | key: synth2-cache 60 | 61 | # Install 62 | - name: Install Dependencies - Ubuntu 63 | if: startsWith(matrix.os,'ubuntu') 64 | run: | 65 | sudo apt-get -yq install libx11-dev 66 | 67 | - name: Setup Visual Studio Command Prompt - Windows 68 | if: startsWith(matrix.os,'windows') 69 | uses: ilammy/msvc-dev-cmd@v1 70 | 71 | - name: Install compiler 72 | uses: dlang-community/setup-dlang@v1 73 | with: 74 | compiler: ${{ matrix.compiler }} 75 | 76 | - name: Install dplug-build 77 | run: | 78 | dub build 79 | working-directory: ./Dplug/tools/dplug-build 80 | 81 | - name: Setup VST2_SDK 82 | if: contains(env.SETUP_VST2_SDK, 'true') && steps.synth2-cache.outputs.cache-hit != 'true' 83 | run: | 84 | curl -LOJ https://web.archive.org/web/20200502121517if_/https://www.steinberg.net/sdk_downloads/vstsdk366_27_06_2016_build_61.zip 85 | 7z x ./vstsdk366_27_06_2016_build_61.zip 86 | mkdir -p ${{ env.VST2_SDK }}/pluginterfaces/vst2.x 87 | cp "./VST3 SDK/pluginterfaces/vst2.x/aeffect.h" ${{ env.VST2_SDK }}/pluginterfaces/vst2.x/aeffect.h 88 | cp "./VST3 SDK/pluginterfaces/vst2.x/aeffectx.h" ${{ env.VST2_SDK }}/pluginterfaces/vst2.x/aeffectx.h 89 | 90 | # Test 91 | - name: Test synth2 92 | # macOS raises link errors 93 | if: startsWith(matrix.os,'macOS') != true 94 | run: | 95 | dub test 96 | 97 | ## Synth2 Plugin 98 | - name: Build synth2 99 | run: | 100 | if ("${{ matrix.os }}" -like 'windows*') { 101 | $Plugins = "-c VST2 -c VST3" 102 | } elseif ("${{ matrix.os }}" -like 'macOS*') { 103 | $Plugins = "-c VST2 -c VST3 -c AU" 104 | } elseif ("${{ matrix.os }}" -like 'ubuntu*') { 105 | $Plugins = "-c VST2 -c VST3 -c LV2" 106 | } 107 | $esc = '--%' 108 | ${{ env.DPlugBuild }} $esc $Plugins --final 109 | working-directory: . 110 | 111 | Skip: 112 | if: "contains(github.event.head_commit.message, '[skip ci]')" 113 | runs-on: ubuntu-latest 114 | steps: 115 | - name: Skip CI 🚫 116 | run: echo skip CI 117 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | # Nightly builds 6 | schedule: 7 | - cron: '00 00 * * *' 8 | 9 | # Common variables for all platforms (ldc is hardcoded in windows job) 10 | env: 11 | VST2_SDK: ${{ github.workspace }}/VST2_SDK 12 | SETUP_VST2_SDK: true 13 | # List of commands 14 | DPlugBuild: ${{ github.workspace }}/Dplug/tools/dplug-build/dplug-build 15 | DplugProcess: ${{ github.workspace }}/Dplug/tools/process/process 16 | 17 | 18 | defaults: 19 | run: 20 | shell: pwsh 21 | 22 | jobs: 23 | Test: 24 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 25 | 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | os: 31 | - windows-latest 32 | - ubuntu-latest 33 | - macOS-latest 34 | compiler: 35 | - 'ldc-latest' 36 | steps: 37 | # Checkout 38 | - name: Checkout master branch 39 | uses: actions/checkout@v2 40 | with: 41 | fetch-depth: 0 42 | 43 | - name: Checkout Dplug repo 44 | uses: actions/checkout@v2 45 | with: 46 | repository: AuburnSounds/Dplug 47 | path: Dplug 48 | 49 | # Cache 50 | - name: Cache 51 | id: synth2-cache 52 | uses: actions/cache@v2 53 | env: 54 | cache-name: synth2-cache 55 | with: 56 | path: | 57 | ${{ env.VST2_SDK }} 58 | key: synth2-cache 59 | 60 | # Install 61 | - name: Install Dependencies - Ubuntu 62 | if: startsWith(matrix.os,'ubuntu') 63 | run: | 64 | sudo apt-get -yq install libx11-dev 65 | 66 | - name: Setup Visual Studio Command Prompt - Windows 67 | if: startsWith(matrix.os,'windows') 68 | uses: ilammy/msvc-dev-cmd@v1 69 | 70 | - name: Install compiler 71 | uses: dlang-community/setup-dlang@v1 72 | with: 73 | compiler: ${{ matrix.compiler }} 74 | 75 | - name: Install dplug-build 76 | run: | 77 | dub build 78 | working-directory: ./Dplug/tools/dplug-build 79 | 80 | - name: Setup VST2_SDK 81 | if: contains(env.SETUP_VST2_SDK, 'true') && steps.synth2-cache.outputs.cache-hit != 'true' 82 | run: | 83 | curl -LOJ https://web.archive.org/web/20200502121517if_/https://www.steinberg.net/sdk_downloads/vstsdk366_27_06_2016_build_61.zip 84 | 7z x ./vstsdk366_27_06_2016_build_61.zip 85 | mkdir -p ${{ env.VST2_SDK }}/pluginterfaces/vst2.x 86 | cp "./VST3 SDK/pluginterfaces/vst2.x/aeffect.h" ${{ env.VST2_SDK }}/pluginterfaces/vst2.x/aeffect.h 87 | cp "./VST3 SDK/pluginterfaces/vst2.x/aeffectx.h" ${{ env.VST2_SDK }}/pluginterfaces/vst2.x/aeffectx.h 88 | 89 | # Test 90 | - name: Test synth2 91 | # macOS raises link errors 92 | if: startsWith(matrix.os,'macOS') != true 93 | run: | 94 | dub test 95 | 96 | ## Synth2 Plugin 97 | - name: Build synth2 98 | run: | 99 | if ("${{ matrix.os }}" -like 'windows*') { 100 | $Plugins = "-c VST2 -c VST3" 101 | } elseif ("${{ matrix.os }}" -like 'macOS*') { 102 | $Plugins = "-c VST2 -c VST3 -c AU -a x86_64" 103 | } elseif ("${{ matrix.os }}" -like 'ubuntu*') { 104 | $Plugins = "-c VST2 -c VST3 -c LV2" 105 | } 106 | $esc = '--%' 107 | ${{ env.DPlugBuild }} $esc $Plugins --final 108 | working-directory: . 109 | 110 | # Upload 111 | # This task uploads the builds directory that contains all artifacts produced by dplug-build 112 | # You may need to repeat this for each plugin that you build 113 | # Pattern matching is not supported here 114 | - name: Upload synth2 115 | uses: actions/upload-artifact@v2 116 | with: 117 | name: synth2-${{ matrix.os }} 118 | path: ./builds/ 119 | 120 | - name: Archive zip 121 | run: | 122 | 7z a ${{ matrix.os }}.zip builds 123 | 124 | - name: Release 125 | uses: softprops/action-gh-release@v1 126 | if: startsWith(github.ref, 'refs/tags/') 127 | with: 128 | files: ${{ matrix.os }}.zip 129 | 130 | Skip: 131 | if: "contains(github.event.head_commit.message, '[skip ci]')" 132 | runs-on: ubuntu-latest 133 | steps: 134 | - name: Skip CI 🚫 135 | run: echo skip CI 136 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: codecov 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | env: 12 | LANG: "en_US.UTF-8" 13 | 14 | jobs: 15 | Test: 16 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 17 | 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | os: 23 | - ubuntu-20.04 24 | arch: 25 | - 'x86_64' 26 | compiler: 27 | - 'dmd-beta' 28 | - 'dmd-latest' 29 | steps: 30 | - name: Checkout master branch 31 | uses: actions/checkout@v2 32 | 33 | - name: Install Dependencies - Ubuntu 34 | if: startsWith(matrix.os,'ubuntu') 35 | run: | 36 | sudo apt-get -yq install libx11-dev 37 | 38 | - name: Install compiler 39 | uses: dlang-community/setup-dlang@v1 40 | with: 41 | compiler: ${{ matrix.compiler }} 42 | 43 | - name: D-Scanner 44 | run: dub fetch dscanner && dub run dscanner -- --styleCheck source 45 | 46 | - name: Test synth2 47 | run: | 48 | dub test -b=unittest-cov 49 | 50 | - uses: codecov/codecov-action@v2 51 | 52 | Skip: 53 | if: "contains(github.event.head_commit.message, '[skip ci]')" 54 | runs-on: ubuntu-20.04 55 | steps: 56 | - name: Skip CI 🚫 57 | run: echo skip CI 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !synth2 4 | !*.d 5 | !*.ini 6 | !*.json 7 | !*.md 8 | !*.py 9 | !.gitignore 10 | !LICENSE 11 | 12 | !tool/ 13 | 14 | !source/ 15 | !source/synth2/ 16 | 17 | !resource/ 18 | !resource/*.ver 19 | !resource/*.lst 20 | !resource/*.png 21 | !resource/*.ttf 22 | 23 | !.github/ 24 | !.github/workflows/ 25 | !.github/workflows/*.yml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Currently closed. Go https://github.com/klknn/kdr 2 | 3 | # Synth2 4 | 5 | [![ci](https://github.com/klknn/synth2/actions/workflows/ci.yml/badge.svg)](https://github.com/klknn/synth2/actions/workflows/ci.yml) 6 | [![codecov](https://codecov.io/gh/klknn/synth2/branch/master/graph/badge.svg?token=4HMC5S2GER)](https://codecov.io/gh/klknn/synth2) 7 | 8 | virtual-analog synth reproducing [synth1](https://www.kvraudio.com/product/synth1-by-daichi-laboratory-ichiro-toda) in D. 9 | 10 | WARNING: this plugin is very unstable. 11 | 12 | ![gui](resource/screen.png) 13 | 14 | ## How to build this plugin? 15 | 16 | https://github.com/AuburnSounds/Dplug/wiki/Getting-Started 17 | 18 | ## Features (TODO) 19 | 20 | - [x] Multi-platform 21 | - [x] VST/VST3/AU CI build 22 | - [x] Windows/Linux CI test (macOS won't be tested because I don't have it) 23 | - [x] Oscillators 24 | - [x] sin/saw/square/triangle/noise waves 25 | - [x] 2nd/sub osc 26 | - [x] detune 27 | - [x] sync 28 | - [x] FM 29 | - [x] AM (ring) 30 | - [x] master control (keyshift/tune/phase/mix/PW) 31 | - [x] mod envelope 32 | - [x] Amplifier 33 | - [x] velocity sensitivity 34 | - [x] ADSR 35 | - [x] Filter 36 | - [x] HP6/HP12/LP6/LP12/LP24/LPDL(TB303 like filter) 37 | - [x] ADSR 38 | - [x] Saturation 39 | - [x] GUI 40 | - [x] LFO 41 | - [x] Effect (phaser is WIP) 42 | - [x] Equalizer / Pan 43 | - [x] Voice 44 | - [x] Tempo Delay 45 | - [x] Chorus / Flanger 46 | - [ ] Unison 47 | - [ ] Reverb 48 | - [ ] Arpeggiator 49 | - [ ] Presets 50 | - [ ] MIDI 51 | - [x] Pitch bend 52 | - [ ] Mod wheel 53 | - [ ] Control change 54 | - [ ] Program change 55 | 56 | 57 | ## History 58 | 59 | - 15 Feb 2021: Fork [poly-alias-synth](https://github.com/AuburnSounds/Dplug/tree/v10.2.1/examples/poly-alias-synth). 60 | -------------------------------------------------------------------------------- /dscanner.ini: -------------------------------------------------------------------------------- 1 | ; Configure which static analysis checks are enabled 2 | [analysis.config.StaticAnalysisConfig] 3 | ; Check variable, class, struct, interface, union, and function names against t 4 | ; he Phobos style guide 5 | style_check="enabled" 6 | ; Check for array literals that cause unnecessary allocation 7 | enum_array_literal_check="enabled" 8 | ; Check for poor exception handling practices 9 | exception_check="enabled" 10 | ; Check for use of the deprecated 'delete' keyword 11 | delete_check="enabled" 12 | ; Check for use of the deprecated floating point operators 13 | float_operator_check="enabled" 14 | ; Check number literals for readability 15 | number_style_check="enabled" 16 | ; Checks that opEquals, opCmp, toHash, and toString are either const, immutable 17 | ; , or inout. 18 | object_const_check="enabled" 19 | ; Checks for .. expressions where the left side is larger than the right. 20 | backwards_range_check="enabled" 21 | ; Checks for if statements whose 'then' block is the same as the 'else' block 22 | if_else_same_check="enabled" 23 | ; Checks for some problems with constructors 24 | constructor_check="enabled" 25 | ; Checks for unused variables 26 | unused_variable_check="enabled" 27 | ; Checks for unused labels 28 | unused_label_check="enabled" 29 | ; Checks for unused function parameters 30 | unused_parameter_check="enabled" 31 | ; Checks for duplicate attributes 32 | duplicate_attribute="enabled" 33 | ; Checks that opEquals and toHash are both defined or neither are defined 34 | opequals_tohash_check="enabled" 35 | ; Checks for subtraction from .length properties 36 | length_subtraction_check="enabled" 37 | ; Checks for methods or properties whose names conflict with built-in propertie 38 | ; s 39 | builtin_property_names_check="enabled" 40 | ; Checks for confusing code in inline asm statements 41 | asm_style_check="enabled" 42 | ; Checks for confusing logical operator precedence 43 | logical_precedence_check="enabled" 44 | ; Checks for undocumented public declarations 45 | undocumented_declaration_check="enabled" 46 | ; Checks for poor placement of function attributes 47 | function_attribute_check="enabled" 48 | ; Checks for use of the comma operator 49 | comma_expression_check="enabled" 50 | ; Checks for local imports that are too broad. Only accurate when checking code 51 | ; used with D versions older than 2.071.0 52 | local_import_check="disabled" 53 | ; Checks for variables that could be declared immutable 54 | could_be_immutable_check="enabled" 55 | ; Checks for redundant expressions in if statements 56 | redundant_if_check="enabled" 57 | ; Checks for redundant parenthesis 58 | redundant_parens_check="enabled" 59 | ; Checks for mismatched argument and parameter names 60 | mismatched_args_check="enabled" 61 | ; Checks for labels with the same name as variables 62 | label_var_same_name_check="enabled" 63 | ; Checks for lines longer than 120 characters 64 | long_line_check="enabled" 65 | ; Checks for assignment to auto-ref function parameters 66 | auto_ref_assignment_check="enabled" 67 | ; Checks for incorrect infinite range definitions 68 | incorrect_infinite_range_check="enabled" 69 | ; Checks for asserts that are always true 70 | useless_assert_check="enabled" 71 | ; Check for uses of the old-style alias syntax 72 | alias_syntax_check="enabled" 73 | ; Checks for else if that should be else static if 74 | static_if_else_check="enabled" 75 | ; Check for unclear lambda syntax 76 | lambda_return_check="enabled" 77 | ; Check for auto function without return statement 78 | auto_function_check="enabled" 79 | ; Check for sortedness of imports 80 | imports_sortedness="disabled" 81 | ; Check for explicitly annotated unittests 82 | explicitly_annotated_unittests="disabled" 83 | ; Check for properly documented public functions (Returns, Params) 84 | properly_documented_public_functions="enabled" 85 | ; Check for useless usage of the final attribute 86 | final_attribute_check="enabled" 87 | ; Check for virtual calls in the class constructors 88 | vcall_in_ctor="enabled" 89 | ; Check for useless user defined initializers 90 | useless_initializer="disabled" 91 | ; Check allman brace style 92 | allman_braces_check="disabled" 93 | ; Check for redundant attributes 94 | redundant_attributes_check="enabled" 95 | ; Check public declarations without a documented unittest 96 | has_public_example="disabled" 97 | ; Check for asserts without an explanatory message 98 | assert_without_msg="disabled" 99 | ; Check indent of if constraints 100 | if_constraints_indent="disabled" 101 | ; Check for @trusted applied to a bigger scope than a single function 102 | trust_too_much="enabled" 103 | ; Check for redundant storage classes on variable declarations 104 | redundant_storage_classes="enabled" 105 | ; Check for unused function return values 106 | unused_result="enabled" 107 | ; ModuleFilters for selectively enabling (+std) and disabling (-std.internal) i 108 | ; ndividual checks 109 | [analysis.config.ModuleFilters] 110 | ; Exclude/Import modules 111 | style_check="" 112 | ; Exclude/Import modules 113 | enum_array_literal_check="" 114 | ; Exclude/Import modules 115 | exception_check="" 116 | ; Exclude/Import modules 117 | delete_check="" 118 | ; Exclude/Import modules 119 | float_operator_check="" 120 | ; Exclude/Import modules 121 | number_style_check="" 122 | ; Exclude/Import modules 123 | object_const_check="" 124 | ; Exclude/Import modules 125 | backwards_range_check="" 126 | ; Exclude/Import modules 127 | if_else_same_check="" 128 | ; Exclude/Import modules 129 | constructor_check="" 130 | ; Exclude/Import modules 131 | unused_variable_check="" 132 | ; Exclude/Import modules 133 | unused_label_check="" 134 | ; Exclude/Import modules 135 | unused_parameter_check="" 136 | ; Exclude/Import modules 137 | duplicate_attribute="" 138 | ; Exclude/Import modules 139 | opequals_tohash_check="" 140 | ; Exclude/Import modules 141 | length_subtraction_check="" 142 | ; Exclude/Import modules 143 | builtin_property_names_check="" 144 | ; Exclude/Import modules 145 | asm_style_check="" 146 | ; Exclude/Import modules 147 | logical_precedence_check="" 148 | ; Exclude/Import modules 149 | undocumented_declaration_check="-synth2.params" 150 | ; Exclude/Import modules 151 | function_attribute_check="" 152 | ; Exclude/Import modules 153 | comma_expression_check="" 154 | ; Exclude/Import modules 155 | local_import_check="" 156 | ; Exclude/Import modules 157 | could_be_immutable_check="" 158 | ; Exclude/Import modules 159 | redundant_if_check="" 160 | ; Exclude/Import modules 161 | redundant_parens_check="" 162 | ; Exclude/Import modules 163 | mismatched_args_check="" 164 | ; Exclude/Import modules 165 | label_var_same_name_check="" 166 | ; Exclude/Import modules 167 | long_line_check="" 168 | ; Exclude/Import modules 169 | auto_ref_assignment_check="" 170 | ; Exclude/Import modules 171 | incorrect_infinite_range_check="" 172 | ; Exclude/Import modules 173 | useless_assert_check="" 174 | ; Exclude/Import modules 175 | alias_syntax_check="" 176 | ; Exclude/Import modules 177 | static_if_else_check="" 178 | ; Exclude/Import modules 179 | lambda_return_check="" 180 | ; Exclude/Import modules 181 | auto_function_check="" 182 | ; Exclude/Import modules 183 | imports_sortedness="" 184 | ; Exclude/Import modules 185 | explicitly_annotated_unittests="" 186 | ; Exclude/Import modules 187 | properly_documented_public_functions="" 188 | ; Exclude/Import modules 189 | final_attribute_check="" 190 | ; Exclude/Import modules 191 | vcall_in_ctor="" 192 | ; Exclude/Import modules 193 | useless_initializer="" 194 | ; Exclude/Import modules 195 | allman_braces_check="" 196 | ; Exclude/Import modules 197 | redundant_attributes_check="" 198 | ; Exclude/Import modules 199 | has_public_example="" 200 | ; Exclude/Import modules 201 | assert_without_msg="" 202 | ; Exclude/Import modules 203 | if_constraints_indent="" 204 | ; Exclude/Import modules 205 | trust_too_much="" 206 | ; Exclude/Import modules 207 | redundant_storage_classes="" 208 | ; Exclude/Import modules 209 | unused_result="" 210 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildTypes": 3 | { 4 | "unittest-opt": 5 | { 6 | "buildOptions": ["unittests", "optimize", "inline"] 7 | }, 8 | "unittest-native": 9 | { 10 | "buildOptions": ["unittests", "optimize", "inline"], 11 | "dflags-ldc": ["-mcpu=native"] 12 | }, 13 | "release-native": 14 | { 15 | "buildOptions": ["releaseMode", "optimize", "inline", "noBoundsCheck"], 16 | "dflags-ldc": ["-mcpu=native"] 17 | } 18 | }, 19 | "configurations": [ 20 | { 21 | "name": "LV2", 22 | "targetType": "dynamicLibrary", 23 | "versions": [ 24 | "LV2" 25 | ], 26 | "dependencies": { 27 | "dplug:lv2": "~>12.7" 28 | }, 29 | "lflags-osx-x86_64-ldc": [ "-exported_symbols_list", "resource/module-lv2.lst", "-dead_strip" ], 30 | "lflags-osx-arm64-ldc": [ "-exported_symbols_list", "resource/module-lv2.lst" ], 31 | "lflags-linux-ldc": [ "--version-script=resource/module-lv2.ver" ], 32 | }, 33 | { 34 | "name": "VST2", 35 | "targetType": "dynamicLibrary", 36 | "versions": [ 37 | "VST2" 38 | ], 39 | "dependencies": { 40 | "dplug:vst2": "~>12.7" 41 | }, 42 | "lflags-osx-x86_64-ldc": [ "-exported_symbols_list", "resource/module-vst.lst", "-dead_strip" ], 43 | "lflags-osx-arm64-ldc": [ "-exported_symbols_list", "resource/module-vst.lst" ], 44 | "lflags-linux-ldc": [ "--version-script=resource/module-vst.ver" ] 45 | }, 46 | { 47 | "name": "VST3", 48 | "targetType": "dynamicLibrary", 49 | "versions": [ 50 | "VST3" 51 | ], 52 | "dependencies": { 53 | "dplug:vst3": "~>12.7" 54 | }, 55 | "lflags-osx-x86_64-ldc": [ "-exported_symbols_list", "resource/module-vst3.lst", "-dead_strip" ], 56 | "lflags-osx-arm64-ldc": [ "-exported_symbols_list", "resource/module-vst3.lst" ], 57 | "lflags-linux-ldc": [ "--version-script=resource/module-vst3.ver" ] 58 | }, 59 | { 60 | "name": "AU", 61 | "versions": ["AU"], 62 | "targetType": "dynamicLibrary", 63 | "dependencies": { 64 | "dplug:au": "~>12.7" 65 | }, 66 | "lflags-osx-x86_64-ldc": [ "-exported_symbols_list", "resource/module-au.lst", "-dead_strip" ], 67 | "lflags-osx-arm64-ldc": [ "-exported_symbols_list", "resource/module-au.lst" ] 68 | }, 69 | ], 70 | "dependencies": { 71 | "dplug:flat-widgets": "~>12.7", 72 | "dplug:pbr-widgets": "~>12.7", 73 | "mir-core": "~>1.3" 74 | }, 75 | "dflags-linux-dmd": [ 76 | "-defaultlib=libphobos2.a" 77 | ], 78 | "dflags-linux-ldc": [ 79 | "-link-defaultlib-shared=false" 80 | ], 81 | "dflags-linux-x86_64-ldc": [ 82 | "-fvisibility=hidden" 83 | ], 84 | "dflags-windows-ldc": ["-mscrtlib=libcmt","-fvisibility=hidden", "-link-defaultlib-shared=false"], 85 | "name": "synth2", 86 | "stringImportPaths": [ 87 | ".", 88 | "resource" 89 | ], 90 | "targetType": "dynamicLibrary" 91 | } 92 | -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "dplug": "12.7.19", 5 | "intel-intrinsics": "1.10.6", 6 | "mir-core": "1.3.6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/AuburnSounds/dplug/master/plugin-schema.json", 3 | "vendorName": "Kl Kn", 4 | "vendorUniqueID": "KlKn", 5 | "vendorSupportEmail": "klknn.gh@gmail.com", 6 | "pluginName": "Synth2", 7 | "pluginHomepage": "https://github.com/klknn/synth2", 8 | "pluginUniqueID": "KKsy", 9 | "publicVersion": "1.0.0", 10 | "CFBundleIdentifierPrefix": "com.klkn", 11 | "hasGUI": true, 12 | "isSynth": true, 13 | "receivesMIDI": true, 14 | "category": "instrumentSynthesizer" 15 | } 16 | -------------------------------------------------------------------------------- /resource/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/synth2/ad31364b2ec3ebd56b1505dfecb10ba07e538a9e/resource/114.png -------------------------------------------------------------------------------- /resource/65.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/synth2/ad31364b2ec3ebd56b1505dfecb10ba07e538a9e/resource/65.png -------------------------------------------------------------------------------- /resource/83.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/synth2/ad31364b2ec3ebd56b1505dfecb10ba07e538a9e/resource/83.png -------------------------------------------------------------------------------- /resource/FORCED SQUARE.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/synth2/ad31364b2ec3ebd56b1505dfecb10ba07e538a9e/resource/FORCED SQUARE.ttf -------------------------------------------------------------------------------- /resource/LeroyLetteringLightBeta01.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/synth2/ad31364b2ec3ebd56b1505dfecb10ba07e538a9e/resource/LeroyLetteringLightBeta01.ttf -------------------------------------------------------------------------------- /resource/TFB.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/synth2/ad31364b2ec3ebd56b1505dfecb10ba07e538a9e/resource/TFB.ttf -------------------------------------------------------------------------------- /resource/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/synth2/ad31364b2ec3ebd56b1505dfecb10ba07e538a9e/resource/black.png -------------------------------------------------------------------------------- /resource/filter_coeff.d: -------------------------------------------------------------------------------- 1 | // -*- mode: d -*- 2 | // DON'T MODIFY THIS FILE AS GENERATED BY tools/filter_coeff.py. 3 | 4 | case FilterKind.LP6: 5 | // === Transfer function === 6 | // H(s) = 1/(s/w0 + 1); 7 | // H(z) = T*w0*(z + 1)/(T*w0*(z + 1) + 2*z - 2); 8 | // #pole = 1 9 | // === Filter coeffients === 10 | nFIR = 2; 11 | nIIR = 1; 12 | b[0] = T*w0/(T*w0 + 2); 13 | b[1] = T*w0/(T*w0 + 2); 14 | a[0] = (T*w0 - 2)/(T*w0 + 2); 15 | return; 16 | 17 | case FilterKind.HP6: 18 | // === Transfer function === 19 | // H(s) = s/(s + w0); 20 | // H(z) = 2*(z - 1)/(T*w0*(z + 1) + 2*z - 2); 21 | // #pole = 1 22 | // === Filter coeffients === 23 | nFIR = 2; 24 | nIIR = 1; 25 | b[0] = 2/(T*w0 + 2); 26 | b[1] = -2/(T*w0 + 2); 27 | a[0] = (T*w0 - 2)/(T*w0 + 2); 28 | return; 29 | 30 | case FilterKind.LP12: 31 | // === Transfer function === 32 | // H(s) = 1/(s^^2/w0^^2 + 1 + s/(Q*w0)); 33 | // H(z) = Q*T^^2*w0^^2*(z + 1)^^2/(Q*T^^2*w0^^2*(z + 1)^^2 + 4*Q*(z - 1)^^2 + 2*T*w0*(z - 1)*(z + 1)); 34 | // #pole = 2 35 | // === Filter coeffients === 36 | nFIR = 3; 37 | nIIR = 2; 38 | b[0] = Q*T^^2*w0^^2/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 39 | b[1] = 2*Q*T^^2*w0^^2/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 40 | b[2] = Q*T^^2*w0^^2/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 41 | a[0] = (2*Q*T^^2*w0^^2 - 8*Q)/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 42 | a[1] = (Q*T^^2*w0^^2 + 4*Q - 2*T*w0)/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 43 | return; 44 | 45 | case FilterKind.HP12: 46 | // === Transfer function === 47 | // H(s) = s^^2/(w0^^2*(s^^2/w0^^2 + 1 + s/(Q*w0))); 48 | // H(z) = 4*Q*(z - 1)^^2/(Q*T^^2*w0^^2*(z + 1)^^2 + 4*Q*(z - 1)^^2 + 2*T*w0*(z - 1)*(z + 1)); 49 | // #pole = 2 50 | // === Filter coeffients === 51 | nFIR = 3; 52 | nIIR = 2; 53 | b[0] = 4*Q/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 54 | b[1] = -8*Q/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 55 | b[2] = 4*Q/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 56 | a[0] = (2*Q*T^^2*w0^^2 - 8*Q)/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 57 | a[1] = (Q*T^^2*w0^^2 + 4*Q - 2*T*w0)/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 58 | return; 59 | 60 | case FilterKind.BP12: 61 | // === Transfer function === 62 | // H(s) = s/(Q*w0*(s^^2/w0^^2 + 1 + s/(Q*w0))); 63 | // H(z) = 2*T*w0*(z - 1)*(z + 1)/(Q*T^^2*w0^^2*(z + 1)^^2 + 4*Q*(z - 1)^^2 + 2*T*w0*(z - 1)*(z + 1)); 64 | // #pole = 2 65 | // === Filter coeffients === 66 | nFIR = 3; 67 | nIIR = 2; 68 | b[0] = 2*T*w0/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 69 | b[1] = 0; 70 | b[2] = -2*T*w0/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 71 | a[0] = (2*Q*T^^2*w0^^2 - 8*Q)/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 72 | a[1] = (Q*T^^2*w0^^2 + 4*Q - 2*T*w0)/(Q*T^^2*w0^^2 + 4*Q + 2*T*w0); 73 | return; 74 | 75 | case FilterKind.LP24: 76 | // Defined in VAFD Sec 5.1, Eq 5.1. 77 | // https://www.discodsp.net/VAFilterDesign_2.1.0.pdf 78 | // === Transfer function === 79 | // H(s) = 1/(Q + s^^4/w0^^4 + 4*s^^3/w0^^3 + 6*s^^2/w0^^2 + 4*s/w0 + 1); 80 | // H(z) = T^^4*w0^^4*(z + 1)^^4/(T^^4*w0^^4*(Q + 1)*(z + 1)^^4 + 8*T^^3*w0^^3*(z - 1)*(z + 1)^^3 + 24*T^^2*w0^^2*(z - 1)^^2*(z + 1)^^2 + 32*T*w0*(z - 1)^^3*(z + 1) + 16*(z - 1)^^4); 81 | // #pole = 4 82 | // === Filter coeffients === 83 | nFIR = 5; 84 | nIIR = 4; 85 | b[0] = T^^4*w0^^4/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 8*T^^3*w0^^3 + 24*T^^2*w0^^2 + 32*T*w0 + 16); 86 | b[1] = 4*T^^4*w0^^4/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 8*T^^3*w0^^3 + 24*T^^2*w0^^2 + 32*T*w0 + 16); 87 | b[2] = 6*T^^4*w0^^4/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 8*T^^3*w0^^3 + 24*T^^2*w0^^2 + 32*T*w0 + 16); 88 | b[3] = 4*T^^4*w0^^4/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 8*T^^3*w0^^3 + 24*T^^2*w0^^2 + 32*T*w0 + 16); 89 | b[4] = T^^4*w0^^4/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 8*T^^3*w0^^3 + 24*T^^2*w0^^2 + 32*T*w0 + 16); 90 | a[0] = (4*Q*T^^4*w0^^4 + 4*T^^4*w0^^4 + 16*T^^3*w0^^3 - 64*T*w0 - 64)/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 8*T^^3*w0^^3 + 24*T^^2*w0^^2 + 32*T*w0 + 16); 91 | a[1] = (6*Q*T^^4*w0^^4 + 6*T^^4*w0^^4 - 48*T^^2*w0^^2 + 96)/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 8*T^^3*w0^^3 + 24*T^^2*w0^^2 + 32*T*w0 + 16); 92 | a[2] = (4*Q*T^^4*w0^^4 + 4*T^^4*w0^^4 - 16*T^^3*w0^^3 + 64*T*w0 - 64)/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 8*T^^3*w0^^3 + 24*T^^2*w0^^2 + 32*T*w0 + 16); 93 | a[3] = (Q*T^^4*w0^^4 + T^^4*w0^^4 - 8*T^^3*w0^^3 + 24*T^^2*w0^^2 - 32*T*w0 + 16)/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 8*T^^3*w0^^3 + 24*T^^2*w0^^2 + 32*T*w0 + 16); 94 | return; 95 | 96 | case FilterKind.LPDL: 97 | // Defined in VAFD Sec 5.10, Eq 5.29. 98 | // https://www.discodsp.net/VAFilterDesign_2.1.0.pdf 99 | // === Transfer function === 100 | // H(s) = 1/(Q + 8*s^^4/w0^^4 + 32*s^^3/w0^^3 + 40*s^^2/w0^^2 + 16*s/w0 + 1); 101 | // H(z) = T^^4*w0^^4*(z + 1)^^4/(T^^4*w0^^4*(Q + 1)*(z + 1)^^4 + 32*T^^3*w0^^3*(z - 1)*(z + 1)^^3 + 160*T^^2*w0^^2*(z - 1)^^2*(z + 1)^^2 + 256*T*w0*(z - 1)^^3*(z + 1) + 128*(z - 1)^^4); 102 | // #pole = 4 103 | // === Filter coeffients === 104 | nFIR = 5; 105 | nIIR = 4; 106 | b[0] = T^^4*w0^^4/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 32*T^^3*w0^^3 + 160*T^^2*w0^^2 + 256*T*w0 + 128); 107 | b[1] = 4*T^^4*w0^^4/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 32*T^^3*w0^^3 + 160*T^^2*w0^^2 + 256*T*w0 + 128); 108 | b[2] = 6*T^^4*w0^^4/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 32*T^^3*w0^^3 + 160*T^^2*w0^^2 + 256*T*w0 + 128); 109 | b[3] = 4*T^^4*w0^^4/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 32*T^^3*w0^^3 + 160*T^^2*w0^^2 + 256*T*w0 + 128); 110 | b[4] = T^^4*w0^^4/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 32*T^^3*w0^^3 + 160*T^^2*w0^^2 + 256*T*w0 + 128); 111 | a[0] = (4*Q*T^^4*w0^^4 + 4*T^^4*w0^^4 + 64*T^^3*w0^^3 - 512*T*w0 - 512)/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 32*T^^3*w0^^3 + 160*T^^2*w0^^2 + 256*T*w0 + 128); 112 | a[1] = (6*Q*T^^4*w0^^4 + 6*T^^4*w0^^4 - 320*T^^2*w0^^2 + 768)/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 32*T^^3*w0^^3 + 160*T^^2*w0^^2 + 256*T*w0 + 128); 113 | a[2] = (4*Q*T^^4*w0^^4 + 4*T^^4*w0^^4 - 64*T^^3*w0^^3 + 512*T*w0 - 512)/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 32*T^^3*w0^^3 + 160*T^^2*w0^^2 + 256*T*w0 + 128); 114 | a[3] = (Q*T^^4*w0^^4 + T^^4*w0^^4 - 32*T^^3*w0^^3 + 160*T^^2*w0^^2 - 256*T*w0 + 128)/(Q*T^^4*w0^^4 + T^^4*w0^^4 + 32*T^^3*w0^^3 + 160*T^^2*w0^^2 + 256*T*w0 + 128); 115 | return; 116 | 117 | -------------------------------------------------------------------------------- /resource/gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/synth2/ad31364b2ec3ebd56b1505dfecb10ba07e538a9e/resource/gray.png -------------------------------------------------------------------------------- /resource/module-au.lst: -------------------------------------------------------------------------------- 1 | _dplugAUEntryPoint 2 | -------------------------------------------------------------------------------- /resource/module-lv2.lst: -------------------------------------------------------------------------------- 1 | _GenerateManifestFromClient 2 | _lv2_descriptor 3 | _lv2ui_descriptor -------------------------------------------------------------------------------- /resource/module-lv2.ver: -------------------------------------------------------------------------------- 1 | LV2ABI_1.0 { 2 | global: lv2_descriptor; lv2ui_descriptor; GenerateManifestFromClient; 3 | local: *; 4 | }; 5 | -------------------------------------------------------------------------------- /resource/module-vst.lst: -------------------------------------------------------------------------------- 1 | _VSTPluginMain 2 | _main_macho 3 | -------------------------------------------------------------------------------- /resource/module-vst.ver: -------------------------------------------------------------------------------- 1 | VSTABI_1.0 { 2 | global: VSTPluginMain; main; 3 | local: *; 4 | }; 5 | -------------------------------------------------------------------------------- /resource/module-vst3.lst: -------------------------------------------------------------------------------- 1 | _InitDll 2 | _ExitDll 3 | _GetPluginFactory 4 | _bundleEntry 5 | _bundleExit 6 | -------------------------------------------------------------------------------- /resource/module-vst3.ver: -------------------------------------------------------------------------------- 1 | VST3ABI_1.0 { 2 | global: GetPluginFactory; ModuleEntry; ModuleExit; 3 | local: *; 4 | }; 5 | -------------------------------------------------------------------------------- /resource/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/synth2/ad31364b2ec3ebd56b1505dfecb10ba07e538a9e/resource/screen.png -------------------------------------------------------------------------------- /resource/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/synth2/ad31364b2ec3ebd56b1505dfecb10ba07e538a9e/resource/white.png -------------------------------------------------------------------------------- /source/synth2/chorus.d: -------------------------------------------------------------------------------- 1 | module synth2.chorus; 2 | 3 | import dplug.client.client : TimeInfo; 4 | 5 | import synth2.delay : Delay, DelayKind; 6 | import synth2.lfo : LFO, Multiplier; 7 | import synth2.waveform : Waveform; 8 | 9 | 10 | /// Chorus adds short modurated delay sounds. 11 | struct Chorus { 12 | @nogc nothrow: 13 | 14 | void setSampleRate(float sampleRate) { 15 | _lfo.setSampleRate(sampleRate); 16 | _delay.setSampleRate(sampleRate); 17 | } 18 | 19 | void setParams(float msecs, float feedback, float depth, float rate) { 20 | _depth = depth; 21 | _msecs = msecs; 22 | _feedback = feedback; 23 | _lfo.setParams(Waveform.sine, false, rate / 10, Multiplier.none, TimeInfo.init); 24 | } 25 | 26 | /// Applies chorus effect. 27 | /// Params: 28 | /// x = dry stereo input. 29 | /// Returns: 30 | /// wet modulated chorus output. 31 | float[2] apply(float[2] x...) { 32 | auto msecsMod = _msecs + (_lfo.front + 1) * _depth; 33 | _lfo.popFront(); 34 | _delay.setParams(DelayKind.st, msecsMod * 1e-3, 0, _feedback); 35 | return _delay.apply(x); 36 | } 37 | 38 | private: 39 | float _depth, _msecs, _feedback; 40 | Delay _delay; 41 | LFO _lfo; 42 | } 43 | 44 | 45 | struct MultiChorus { 46 | @nogc nothrow: 47 | 48 | static immutable offsetMSecs = [0.55, 0.64, 12.5, 26.4, 18.4]; 49 | 50 | void setSampleRate(float sampleRate) { 51 | foreach (ref c; _chorus) { 52 | c.setSampleRate(sampleRate); 53 | } 54 | } 55 | 56 | void setParams(int numActive, float width, 57 | float msecs, float feedback, float depth, float rate) { 58 | _numActive = numActive; 59 | _width = width; 60 | foreach (i, ref c; _chorus) { 61 | c.setParams(msecs + offsetMSecs[i], feedback, depth, rate); 62 | } 63 | } 64 | 65 | float[2] apply(float[2] x...) { 66 | float[2] y; 67 | y[] = 0; 68 | if (_width == 0 || _numActive == 1) { 69 | foreach (i; 0 .. _numActive) { 70 | y[] += _chorus[i].apply(x)[]; 71 | } 72 | } 73 | // Wide stereo panning. 74 | else { 75 | const width = _width / 2 + 0.5; // range [0.5, 1.0] 76 | if (_numActive >= 2) { 77 | const c0 = _chorus[0].apply(x); 78 | y[0] += width * c0[0]; 79 | y[1] += (1 - width) * c0[1]; 80 | const c1 = _chorus[1].apply(x); 81 | y[0] += (1 - width) * c1[0]; 82 | y[1] += width * c1[1]; 83 | } 84 | if (_numActive == 3) { 85 | y[] += _chorus[2].apply(x)[]; 86 | } 87 | if (_numActive == 4) { 88 | const halfWidth = _width / 2 + 0.5; // range [0.5, 0.75] 89 | const c2 = _chorus[2].apply(x); 90 | y[0] += halfWidth * c2[0]; 91 | y[1] += (1 - halfWidth) * c2[1]; 92 | const c3 = _chorus[3].apply(x); 93 | y[0] += (1 - halfWidth) * c3[0]; 94 | y[1] += halfWidth * c3[1]; 95 | } 96 | } 97 | y[] /= _numActive; 98 | return y; 99 | } 100 | 101 | private: 102 | float _width; 103 | int _numActive; 104 | Chorus[4] _chorus; 105 | } 106 | -------------------------------------------------------------------------------- /source/synth2/client.d: -------------------------------------------------------------------------------- 1 | /** 2 | Synth2 virtual analog syntesizer. 3 | 4 | Copyright: klknn 2021. 5 | Copyright: Elias Batek 2018. 6 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 7 | */ 8 | module synth2.client; 9 | 10 | import std.algorithm.comparison : clamp; 11 | import std.traits : EnumMembers; 12 | import std.math : tanh; 13 | 14 | import dplug.core.math : convertDecibelToLinearGain; 15 | import dplug.core.nogc : destroyFree, mallocNew; 16 | import dplug.core.vec : makeVec, Vec; 17 | import dplug.client.client : Client, LegalIO, parsePluginInfo, PluginInfo, TimeInfo; 18 | import dplug.client.graphics : IGraphics; 19 | import dplug.client.params : Parameter; 20 | import dplug.client.midi : MidiMessage, makeMidiMessageNoteOn, makeMidiMessageNoteOff; 21 | import mir.math : exp2, log, sqrt, PI, fastmath; 22 | 23 | import synth2.chorus : MultiChorus; 24 | import synth2.delay : Delay, DelayKind; 25 | import synth2.equalizer : Equalizer; 26 | import synth2.effect : EffectKind, MultiEffect; 27 | import synth2.envelope : ADSR; 28 | import synth2.filter : FilterKind; 29 | import synth2.modfilter : ModFilter; 30 | import synth2.lfo : Interval, LFO, Multiplier, toBar, toSeconds; 31 | version (unittest) {} else import synth2.gui : Synth2GUI; 32 | import synth2.oscillator : Oscillator; 33 | import synth2.waveform : Waveform; 34 | import synth2.params : Params, ParamBuilder, paramNames, MEnvDest, LfoDest, VoiceKind; 35 | 36 | version (unittest) {} else { 37 | import dplug.client.dllmain : DLLEntryPoint, pluginEntryPoints; 38 | 39 | // This define entry points for plugin formats, 40 | // depending on which version identifiers are defined. 41 | mixin(pluginEntryPoints!Synth2Client); 42 | } 43 | 44 | /// Polyphonic digital-aliasing synth 45 | class Synth2Client : Client { 46 | public: 47 | nothrow @nogc @fastmath: 48 | 49 | /// ctor. 50 | this() { 51 | super(); 52 | _effect = mallocNew!MultiEffect; 53 | } 54 | 55 | ~this() { 56 | destroyFree(_effect); 57 | } 58 | 59 | // NOTE: this method will not call until GUI required (lazy) 60 | version (unittest) {} else 61 | override IGraphics createGraphics() { 62 | _gui = mallocNew!Synth2GUI( 63 | this.params, 64 | ); 65 | return _gui; 66 | } 67 | 68 | override PluginInfo buildPluginInfo() { 69 | // Plugin info is parsed from plugin.json here at compile time. 70 | static immutable info = parsePluginInfo(import("plugin.json")); 71 | return info; 72 | } 73 | 74 | override Parameter[] buildParameters() { 75 | return ParamBuilder.buildParameters(); 76 | } 77 | 78 | override LegalIO[] buildLegalIO() { 79 | auto io = makeVec!LegalIO(); 80 | io ~= LegalIO(0, 1); 81 | io ~= LegalIO(0, 2); 82 | return io.releaseData(); 83 | } 84 | 85 | override int maxFramesInProcess() pure { 86 | return 32; // samples only processed by a maximum of 32 samples 87 | } 88 | 89 | override void reset(double sampleRate, int maxFrames, 90 | int numInputs, int numOutputs) { 91 | foreach (ref o; _osc1s) { 92 | o.setSampleRate(sampleRate); 93 | } 94 | _osc2.setSampleRate(sampleRate); 95 | _oscSub.setSampleRate(sampleRate); 96 | _filter.setSampleRate(sampleRate); 97 | _menv.setSampleRate(sampleRate); 98 | _menv.sustainLevel = 0; 99 | _menv.releaseTime = 0; 100 | _effect.setSampleRate(sampleRate); 101 | _delay.setSampleRate(sampleRate); 102 | _chorus.setSampleRate(sampleRate); 103 | _eq.setSampleRate(sampleRate); 104 | foreach (ref lfo; _lfos) { 105 | lfo.setSampleRate(sampleRate); 106 | } 107 | } 108 | 109 | override void processAudio(const(float*)[] inputs, float*[] outputs, 110 | int frames, TimeInfo info) { 111 | // TODO: update tempo by a parameter listener. 112 | version (unittest) { 113 | } else { 114 | if (_gui) { 115 | _gui.setTempo(info.tempo); 116 | } 117 | } 118 | 119 | const poly = readParam!int(Params.voicePoly); 120 | const voiceKind = readParam!VoiceKind(Params.voiceKind); 121 | const portament = readParam!float(Params.voicePortament) - ParamBuilder.logBias; 122 | const autoPortament = readParam!bool(Params.voicePortamentAuto); 123 | const legato = voiceKind == VoiceKind.legato; 124 | const maxVoices = voiceKind == VoiceKind.poly ? poly : 1; 125 | 126 | const ampGain = exp2(readParam!float(Params.ampGain)); 127 | if (ampGain == 0) return; // no output 128 | 129 | // Setup LFOs. 130 | LfoDest[nLFO] lfoDests; 131 | float[nLFO] lfoAmounts; 132 | bool[nLFO] lfoTriggers; 133 | lfoAmounts[0] = readParam!float(Params.lfo1Amount); 134 | if (lfoAmounts[0] > 0) { 135 | lfoDests[0] = readParam!LfoDest(Params.lfo1Dest); 136 | _lfos[0].setParams( 137 | readParam!Waveform(Params.lfo1Wave), 138 | readParam!bool(Params.lfo1Sync), 139 | readParam!float(Params.lfo1Speed), 140 | readParam!Multiplier(Params.lfo1Mul), info); 141 | lfoTriggers[0] = readParam!bool(Params.lfo1Trigger); 142 | } 143 | lfoAmounts[1] = readParam!float(Params.lfo2Amount); 144 | if (lfoAmounts[1] > 0) { 145 | lfoDests[1] = readParam!LfoDest(Params.lfo2Dest); 146 | _lfos[1].setParams( 147 | readParam!Waveform(Params.lfo2Wave), 148 | readParam!bool(Params.lfo2Sync), 149 | readParam!float(Params.lfo2Speed), 150 | readParam!Multiplier(Params.lfo2Mul), info); 151 | lfoTriggers[1] = readParam!bool(Params.lfo2Trigger); 152 | } 153 | 154 | // Setup OSCs. 155 | const oscMix = readParam!float(Params.oscMix); 156 | const sync = readParam!bool(Params.osc2Sync); 157 | const ring = readParam!bool(Params.osc2Ring); 158 | const fm = readParam!float(Params.osc1FM); 159 | const menvDest = readParam!MEnvDest(Params.menvDest); 160 | const menvAmount = readParam!float(Params.menvAmount); 161 | bool lfoDoFM = false; 162 | foreach (i, dst; lfoDests) { 163 | lfoDoFM |= (lfoAmounts[i] > 0 && dst == LfoDest.fm); 164 | } 165 | const doFM = !sync && !ring && (fm > 0 || (menvAmount != 0 && menvDest == MEnvDest.fm) || lfoDoFM); 166 | 167 | const attack = readParam!float(Params.ampAttack) - ParamBuilder.logBias; 168 | const decay = readParam!float(Params.ampDecay) - ParamBuilder.logBias; 169 | const sustain = exp2(readParam!float(Params.ampSustain)); 170 | const release = readParam!float(Params.ampRelease) - ParamBuilder.logBias; 171 | 172 | const oscKeyShift = readParam!int(Params.oscKeyShift); 173 | const oscTune = readParam!float(Params.oscTune); 174 | const oscPhase = readParam!float(Params.oscPhase); 175 | const pw = readParam!float(Params.oscPulseWidth); 176 | const vel = readParam!float(Params.ampVel); 177 | 178 | const useOsc1 = oscMix != 1 || sync || ring || fm > 0; 179 | const osc1Det = readParam!float(Params.osc1Det); 180 | const useOsc1Det = osc1Det != 0; 181 | const useOsc2 = oscMix != 0 || sync || ring || fm > 0; 182 | const oscSubVol = exp2(readParam!float(Params.oscSubVol)); 183 | const useOscSub = oscSubVol != 0; 184 | 185 | const osc1NoteDiff = oscKeyShift + oscTune; 186 | if (useOsc1) { 187 | foreach (i, ref Oscillator _osc1; _osc1s) { 188 | _osc1.setVoice(maxVoices, legato, portament, autoPortament); 189 | _osc1.setWaveform(readParam!Waveform(Params.osc1Waveform)); 190 | _osc1.setPulseWidth(pw); 191 | _osc1.setVelocitySense(vel); 192 | _osc1.setADSR(attack, decay, sustain, release); 193 | _osc1.setNoteDiff(osc1NoteDiff); 194 | if (oscPhase != ParamBuilder.ignoreOscPhase) { 195 | _osc1.setInitialPhase(oscPhase); 196 | } 197 | if (osc1Det == 0) break; // skip detuned osc1s 198 | _osc1.setNoteDetune(log(osc1Det + 1f) * 2 * 199 | log(i + 1f) / log(cast(float) _osc1s.length)); 200 | } 201 | } 202 | 203 | const osc2NoteDiff = oscKeyShift + oscTune + readParam!int(Params.osc2Pitch) 204 | + readParam!float(Params.osc2Fine); 205 | if (useOsc2) { 206 | _osc2.setVoice(maxVoices, legato, portament, autoPortament); 207 | _osc2.setWaveform(readParam!Waveform(Params.osc2Waveform)); 208 | _osc2.setPulseWidth(pw); 209 | _osc2.setNoteTrack(readParam!bool(Params.osc2Track)); 210 | _osc2.setNoteDiff(osc2NoteDiff); 211 | _osc2.setVelocitySense(vel); 212 | _osc2.setADSR(attack, decay, sustain, release); 213 | if (oscPhase != ParamBuilder.ignoreOscPhase) { 214 | _osc2.setInitialPhase(oscPhase); 215 | } 216 | } 217 | 218 | if (oscSubVol != 0) { 219 | _oscSub.setVoice(maxVoices, legato, portament, autoPortament); 220 | _oscSub.setWaveform(readParam!Waveform(Params.oscSubWaveform)); 221 | _oscSub.setNoteDiff( 222 | oscKeyShift + oscTune + readParam!bool(Params.oscSubOct) ? -12 : 0); 223 | _oscSub.setVelocitySense(vel); 224 | _oscSub.setADSR(attack, decay, sustain, release); 225 | if (oscPhase != ParamBuilder.ignoreOscPhase) { 226 | _oscSub.setInitialPhase(oscPhase); 227 | } 228 | } 229 | 230 | // Setup filter. 231 | _filter.useVelocity = readParam!bool(Params.filterUseVelocity); 232 | _filter.trackAmount = readParam!float(Params.filterTrack); 233 | _filter.envAmount = readParam!float(Params.filterEnvAmount); 234 | if (_filter.envAmount != 0) { 235 | _filter.envelope.attackTime = 236 | readParam!float(Params.filterAttack) - ParamBuilder.logBias; 237 | _filter.envelope.decayTime = 238 | readParam!float(Params.filterDecay) - ParamBuilder.logBias; 239 | _filter.envelope.sustainLevel = exp2(readParam!float(Params.filterSustain)); 240 | _filter.envelope.releaseTime = 241 | readParam!float(Params.filterRelease) - ParamBuilder.logBias; 242 | } 243 | _filter.setParams( 244 | readParam!FilterKind(Params.filterKind), 245 | readParam!float(Params.filterCutoff), 246 | readParam!float(Params.filterQ)); 247 | const saturation = readParam!float(Params.saturation); 248 | const satNorm = tanh(saturation); 249 | 250 | // Setup freq by MIDI and params. 251 | foreach (msg; this.getNextMidiMessages(frames)) { 252 | if (useOsc1) { 253 | foreach (ref o1; _osc1s) { 254 | o1.setMidi(msg); 255 | if (!useOsc1Det) break; 256 | } 257 | } 258 | if (useOsc2) _osc2.setMidi(msg); 259 | if (useOscSub) _oscSub.setMidi(msg); 260 | _filter.setMidi(msg); 261 | _menv.setMidi(msg); 262 | foreach (i; 0 .. nLFO) { 263 | if (lfoTriggers[i]) _lfos[i].setMidi(msg); 264 | } 265 | } 266 | 267 | if (useOsc1) { 268 | foreach (ref o; _osc1s) { 269 | o.updateFreq(); 270 | if (!useOsc1Det) break; 271 | } 272 | } 273 | if (useOsc2) _osc2.updateFreq(); 274 | if (useOscSub) _oscSub.updateFreq(); 275 | 276 | _menv.attackTime = readParam!float(Params.menvAttack); 277 | _menv.decayTime = readParam!float(Params.menvDecay); 278 | 279 | // Setup _effect. 280 | const effectMix = readParam!float(Params.effectMix); 281 | if (effectMix != 0) { 282 | _effect.setEffectKind(readParam!EffectKind(Params.effectKind)); 283 | _effect.setParams(readParam!float(Params.effectCtrl1), 284 | readParam!float(Params.effectCtrl2)); 285 | } 286 | 287 | _eq.setParams(readParam!float(Params.eqLevel), 288 | readParam!float(Params.eqFreq), 289 | readParam!float(Params.eqQ), 290 | readParam!float(Params.eqTone)); 291 | const eqPan = -readParam!float(Params.eqPan); 292 | 293 | // Setup delay. 294 | const delayMix = readParam!float(Params.delayMix); 295 | if (delayMix != 0) { 296 | const delayInterval = Interval(toBar(readParam!float(Params.delayTime)), 297 | readParam!Multiplier(Params.delayMul)); 298 | _delay.setParams( 299 | readParam!DelayKind(Params.delayKind), 300 | toSeconds(delayInterval, info.tempo), 301 | readParam!float(Params.delaySpread), 302 | readParam!float(Params.delayFeedback)); 303 | } 304 | 305 | // Setup chorus. 306 | // TODO: support Params.chorusMulti and width. 307 | const chorusLevel = convertDecibelToLinearGain( 308 | readParam!float(Params.chorusLevel)); 309 | const chorusOn = readParam!bool(Params.chorusOn) && chorusLevel > 0; 310 | if (chorusOn) { 311 | _chorus.setParams( 312 | readParam!int(Params.chorusMulti), 313 | readParam!float(Params.chorusWidth), 314 | readParam!float(Params.chorusTime), 315 | readParam!float(Params.chorusFeedback), 316 | readParam!float(Params.chorusDepth), 317 | readParam!float(Params.chorusRate)); 318 | } 319 | 320 | // Generate samples. 321 | foreach (frame; 0 .. frames) { 322 | float menvVal = menvAmount * _menv.front; 323 | _menv.popFront(); 324 | float[nLFO] lfoVals; 325 | foreach (i; 0 .. nLFO) { 326 | lfoVals[i] = lfoAmounts[i] * _lfos[i].front(); 327 | _lfos[i].popFront(); 328 | } 329 | 330 | // modulation 331 | float modPW = pw; 332 | float modFM = fm; 333 | float modOsc1NoteDiff = osc1NoteDiff; 334 | float modOsc2NoteDiff = osc2NoteDiff; 335 | float modAmp = ampGain; 336 | float modPan = eqPan; 337 | float modCutoff = 0; 338 | final switch (menvDest) { 339 | case MEnvDest.pw: modPW += menvVal; break; 340 | case MEnvDest.fm: modFM += menvVal; break; 341 | case MEnvDest.osc2: modOsc2NoteDiff += menvVal; break; 342 | } 343 | foreach (i; 0 .. nLFO) { 344 | final switch (lfoDests[i]) { 345 | case LfoDest.pw: modPW += lfoVals[i]; break; 346 | case LfoDest.fm: modFM += lfoVals[i]; break; 347 | case LfoDest.osc12: modOsc1NoteDiff += lfoVals[i]; goto case LfoDest.osc2; 348 | case LfoDest.osc2: modOsc2NoteDiff += lfoVals[i]; break; 349 | case LfoDest.amp: modAmp += lfoVals[i]; break; // maybe *=? 350 | case LfoDest.pan: modPan += lfoVals[i]; break; 351 | case LfoDest.filter: modCutoff += lfoVals[i]; break; 352 | } 353 | } 354 | 355 | // osc1 356 | float o1 = 0; 357 | if (useOsc1) { 358 | foreach (ref Oscillator o; _osc1s) { 359 | if (modPW != pw) o.setPulseWidth(modPW); 360 | if (doFM) o.setFM(modFM, _osc2); 361 | if (modOsc1NoteDiff != osc1NoteDiff) { 362 | o.setNoteDiff(modOsc1NoteDiff); 363 | o.updateFreq(); 364 | } 365 | o1 += o.front; 366 | o.popFront(); 367 | if (osc1Det == 0) break; 368 | } 369 | } 370 | float output = (1.0 - oscMix) * o1; 371 | 372 | // osc2 373 | if (useOsc2) { 374 | if (modPW != pw) { 375 | _osc2.setPulseWidth(modPW); 376 | } 377 | if (modOsc2NoteDiff != osc2NoteDiff) { 378 | _osc2.setNoteDiff(modOsc2NoteDiff); 379 | _osc2.updateFreq(); 380 | } 381 | _osc2.setPulseWidth(pw + menvVal); 382 | if (sync) { 383 | _osc2.synchronize(_osc1s[0]); 384 | } 385 | output += oscMix * _osc2.front * (ring ? o1 : 1f); 386 | _osc2.popFront(); 387 | } 388 | 389 | // oscSub 390 | if (useOscSub) { 391 | if (menvDest == MEnvDest.pw) { 392 | _oscSub.setPulseWidth(pw + menvVal); 393 | } 394 | output += oscSubVol * _oscSub.front; 395 | _oscSub.popFront(); 396 | } 397 | 398 | if (saturation != 0) { 399 | output = tanh(saturation * output) / satNorm; 400 | } 401 | 402 | // filter 403 | _filter.setCutoffDiff(modCutoff); 404 | output = _filter.apply(output); 405 | if (effectMix != 0) { 406 | output = effectMix * _effect.apply(output) + (1f - effectMix) * output; 407 | } 408 | output = _eq.apply(output); 409 | 410 | output *= modAmp; 411 | outputs[0][frame] = (1 + modPan) * output; 412 | outputs[1][frame] = (1 - modPan) * output; 413 | 414 | if (chorusOn) { 415 | const chorusOuts = _chorus.apply(outputs[0][frame], outputs[1][frame]); 416 | foreach (i; 0 .. outputs.length) { 417 | outputs[i][frame] += chorusLevel * chorusOuts[i]; 418 | } 419 | } 420 | 421 | if (delayMix != 0) { 422 | const delayOuts = _delay.apply(outputs[0][frame], outputs[1][frame]); 423 | foreach (i; 0 .. outputs.length) { 424 | outputs[i][frame] = (1 - delayMix) * outputs[i][frame] + delayMix * delayOuts[i]; 425 | } 426 | } 427 | _filter.popFront(); 428 | _menv.popFront(); 429 | } 430 | } 431 | 432 | private: 433 | enum nLFO = 2; 434 | MultiChorus _chorus; 435 | Delay _delay; 436 | LFO[nLFO] _lfos; 437 | MultiEffect _effect; 438 | ModFilter _filter; 439 | ADSR _menv; 440 | Equalizer _eq; 441 | Oscillator _osc2, _oscSub; 442 | Oscillator[8] _osc1s; // +7 for detune 443 | version (unittest) {} else Synth2GUI _gui; 444 | } 445 | 446 | 447 | /// Mock host for testing Synth2Client. 448 | private struct TestHost { 449 | Synth2Client client; 450 | int frames = 8; 451 | Vec!float[2] outputFrames; 452 | MidiMessage msg1 = makeMidiMessageNoteOn(0, 0, 100, 100); 453 | MidiMessage msg2 = makeMidiMessageNoteOn(1, 0, 90, 10); 454 | MidiMessage msg3 = makeMidiMessageNoteOff(2, 0, 100); 455 | bool noteOff = false; 456 | 457 | @nogc nothrow: 458 | 459 | /// Sets param to test. 460 | private void setParam(Params pid, T)(T val) { 461 | auto p = __traits(getMember, ParamBuilder, paramNames[pid]); 462 | static if (is(T == enum)) { 463 | double v; 464 | v = double.init; // for d-scanner. 465 | static immutable names = [__traits(allMembers, T)]; 466 | assert(p.normalizedValueFromString(names[val], v)); 467 | } 468 | else static if (is(T == bool)) { 469 | auto v = val ? 1.0 : 0.0; 470 | } 471 | else static if (is(T == int)) { 472 | auto v = clamp((cast(double)val - p.minValue) / 473 | (p.maxValue - p.minValue), 0.0, 1.0); 474 | } 475 | else static if (is(T : double)) { 476 | auto v = p.toNormalized(val); 477 | } 478 | else { 479 | static assert(false, "unknown param"); 480 | } 481 | client.param(pid).setFromHost(v); 482 | } 483 | 484 | void processAudio() { 485 | outputFrames[0].resize(this.frames); 486 | outputFrames[1].resize(this.frames); 487 | client.reset(44_100, 32, 0, 2); 488 | 489 | float*[2] inputs, outputs; 490 | inputs[0] = null; 491 | inputs[1] = null; 492 | outputs[0] = &outputFrames[0][0]; 493 | outputs[1] = &outputFrames[1][0]; 494 | 495 | client.enqueueMIDIFromHost(msg1); 496 | client.enqueueMIDIFromHost(msg2); 497 | if (noteOff) { 498 | client.enqueueMIDIFromHost(msg3); 499 | } 500 | 501 | TimeInfo info; 502 | info.hostIsPlaying = true; 503 | client.processAudioFromHost(inputs[], outputs[], frames, info); 504 | } 505 | 506 | /// Returns true iff the val changes outputs of processAudio. 507 | bool paramChangeOutputs(Params pid, T)(T val) { 508 | const double origin = this.client.param(pid).getForHost; 509 | 510 | // 1st trial w/o param 511 | this.processAudio(); 512 | auto prev = makeVec!float(this.frames); 513 | foreach (i; 0 .. frames) { 514 | prev[i] = outputFrames[0][i]; 515 | } 516 | this.setParam!(pid, T)(val); 517 | 518 | // 2nd trial w/ param 519 | this.processAudio(); 520 | 521 | // revert param 522 | this.client.param(pid).setFromHost(origin); 523 | 524 | foreach (i; 0 .. frames) { 525 | if (prev[i] != outputFrames[0][i]) 526 | return true; 527 | } 528 | return false; 529 | } 530 | } 531 | 532 | /// Test default params with benchmark. 533 | @nogc nothrow @system 534 | unittest { 535 | import core.stdc.stdio : printf; 536 | import std.datetime.stopwatch : benchmark; 537 | 538 | TestHost host = { client: mallocNew!Synth2Client(), frames: 100 }; 539 | scope (exit) destroyFree(host.client); 540 | 541 | host.processAudio(); // to omit the first record. 542 | auto time = benchmark!(() => host.processAudio())(100)[0].split!("msecs", "usecs"); 543 | printf("benchmark (default): %d ms %d us\n", cast(int) time.msecs, cast(int) time.usecs); 544 | version (OSX) {} else { 545 | version (LDC) assert(time.msecs <= 20); 546 | } 547 | } 548 | 549 | /// Test deterministic outputs. 550 | @nogc nothrow @system 551 | unittest { 552 | enum N = 100; 553 | float[N] prev; 554 | TestHost host = { client: mallocNew!Synth2Client(), frames: N }; 555 | scope (exit) destroyFree(host.client); 556 | 557 | foreach (noteOff; [false, true]) { 558 | // 1st 559 | host.noteOff = noteOff; 560 | host.processAudio(); 561 | prev[] = host.outputFrames[0][]; 562 | bool notNaN = true; 563 | foreach (x; prev) { 564 | notNaN &= x != float.init; 565 | } 566 | assert(notNaN); 567 | 568 | // 2nd 569 | host.processAudio(); 570 | assert(prev[] == host.outputFrames[0][]); 571 | } 572 | } 573 | 574 | /// Test pitch bend. 575 | @nogc nothrow @system 576 | unittest { 577 | import dplug.client.midi : makeMidiMessagePitchWheel; 578 | enum N = 100; 579 | float[N] prev; 580 | TestHost host = { client: mallocNew!Synth2Client(), frames: N }; 581 | scope (exit) destroyFree(host.client); 582 | 583 | host.processAudio(); 584 | prev[] = host.outputFrames[0][]; 585 | 586 | host.client.enqueueMIDIFromHost(makeMidiMessagePitchWheel( 587 | 0, 0, 1)); 588 | host.processAudio(); 589 | assert(prev[] != host.outputFrames[0][]); 590 | } 591 | 592 | 593 | /// Test changing waveforms. 594 | @nogc nothrow @system 595 | unittest { 596 | TestHost host = { mallocNew!Synth2Client() }; 597 | scope (exit) destroyFree(host.client); 598 | 599 | host.setParam!(Params.oscMix)(0.5); 600 | host.setParam!(Params.oscSubVol)(0.5); 601 | foreach (wf; EnumMembers!Waveform) { 602 | host.setParam!(Params.osc1Waveform)(wf); 603 | host.setParam!(Params.osc2Waveform)(wf); 604 | host.setParam!(Params.oscSubWaveform)(wf); 605 | host.processAudio(); 606 | assert(host.client._osc1s[0].lastUsedWave.waveform == wf); 607 | assert(host.client._osc2.lastUsedWave.waveform == wf); 608 | assert(host.client._oscSub.lastUsedWave.waveform == wf); 609 | } 610 | } 611 | 612 | /// Test FM (TODO: check values). 613 | @nogc nothrow @system 614 | unittest { 615 | TestHost host = { mallocNew!Synth2Client() }; 616 | scope (exit) destroyFree(host.client); 617 | 618 | assert(host.paramChangeOutputs!(Params.osc1FM)(10.0)); 619 | } 620 | 621 | /// Test detune. 622 | @nogc nothrow @system 623 | unittest { 624 | TestHost host = { mallocNew!Synth2Client() }; 625 | scope (exit) destroyFree(host.client); 626 | 627 | assert(host.paramChangeOutputs!(Params.osc1Det)(1.0)); 628 | 629 | host.processAudio(); 630 | // Check the detune osc1s are NOT playing. 631 | foreach (o; host.client._osc1s[1 .. $]) { 632 | assert(!o.isPlaying); 633 | } 634 | 635 | host.setParam!(Params.osc1Det)(1.0); 636 | host.processAudio(); 637 | // Check all the osc1s are playing. 638 | foreach (o; host.client._osc1s) { 639 | assert(o.isPlaying); 640 | } 641 | } 642 | 643 | /// Test oscKeyShift/oscTune/oscPhase. 644 | @nogc nothrow @system 645 | unittest { 646 | TestHost host = { mallocNew!Synth2Client() }; 647 | scope (exit) destroyFree(host.client); 648 | 649 | foreach (mix; [0.0, 1.0]) { 650 | host.setParam!(Params.oscMix)(mix); 651 | host.setParam!(Params.oscSubVol)(1.0); 652 | assert(host.paramChangeOutputs!(Params.oscKeyShift)(12)); 653 | assert(host.paramChangeOutputs!(Params.oscTune)(0.5)); 654 | assert(host.paramChangeOutputs!(Params.oscPhase)(0.5)); 655 | } 656 | } 657 | 658 | /// Test Osc2 Track and pitch 659 | @nogc nothrow @system 660 | unittest { 661 | import std.math : isClose; 662 | TestHost host = { mallocNew!Synth2Client() }; 663 | scope (exit) destroyFree(host.client); 664 | 665 | // Check initial pitch. 666 | host.setParam!(Params.oscMix)(1.0); 667 | host.setParam!(Params.osc2Track)(false); 668 | host.processAudio(); 669 | assert(isClose(host.client._osc2.lastUsedWave.freq, 440)); 670 | 671 | // Check pitch is 1 octave down. 672 | host.setParam!(Params.osc2Pitch)(-12); 673 | host.processAudio(); 674 | assert(isClose(host.client._osc2.lastUsedWave.freq, 220)); 675 | 676 | // Check pitch is down from 220hz. 677 | host.setParam!(Params.osc2Fine)(-1.0); 678 | host.processAudio(); 679 | assert(host.client._osc2.lastUsedWave.freq < 220); 680 | } 681 | 682 | /// Test sync. 683 | @nogc nothrow @system 684 | unittest { 685 | TestHost host = { mallocNew!Synth2Client() }; 686 | scope (exit) destroyFree(host.client); 687 | 688 | host.frames = 100; 689 | host.setParam!(Params.oscMix)(1.0); 690 | host.setParam!(Params.osc2Pitch)(-2); 691 | assert(host.paramChangeOutputs!(Params.osc2Sync)(true)); 692 | } 693 | 694 | /// Test ring. 695 | @nogc nothrow @system 696 | unittest { 697 | TestHost host = { mallocNew!Synth2Client() }; 698 | scope (exit) destroyFree(host.client); 699 | 700 | host.setParam!(Params.oscMix)(1.0); 701 | assert(host.paramChangeOutputs!(Params.osc2Ring)(true)); 702 | } 703 | 704 | /// Test pulse width. 705 | @nogc nothrow @system 706 | unittest { 707 | TestHost host = { mallocNew!Synth2Client() }; 708 | scope (exit) destroyFree(host.client); 709 | 710 | // PW does NOT work for Waveform != pulse. 711 | host.setParam!(Params.osc1Waveform)(Waveform.sine); 712 | assert(!host.paramChangeOutputs!(Params.oscPulseWidth)(0.1)); 713 | 714 | // PW only works for Waveform.pulse. 715 | host.setParam!(Params.osc1Waveform)(Waveform.pulse); 716 | assert(host.paramChangeOutputs!(Params.oscPulseWidth)(0.1)); 717 | } 718 | 719 | /// Test oscSubVol. 720 | @nogc nothrow @system 721 | unittest { 722 | TestHost host = { mallocNew!Synth2Client() }; 723 | scope (exit) destroyFree(host.client); 724 | 725 | assert(host.paramChangeOutputs!(Params.oscSubVol)(1.0)); 726 | } 727 | 728 | /// Test ampVel. 729 | @nogc nothrow @system 730 | unittest { 731 | TestHost host = { mallocNew!Synth2Client() }; 732 | scope (exit) destroyFree(host.client); 733 | assert(host.paramChangeOutputs!(Params.ampVel)(0.0)); 734 | } 735 | 736 | /// Test filter 737 | @nogc nothrow @system 738 | unittest { 739 | TestHost host = { mallocNew!Synth2Client() }; 740 | scope (exit) destroyFree(host.client); 741 | foreach (fkind; EnumMembers!FilterKind) { 742 | host.setParam!(Params.filterKind)(fkind); 743 | assert(host.paramChangeOutputs!(Params.filterCutoff)(0.5)); 744 | if (fkind != FilterKind.HP6 && fkind != FilterKind.LP6) { 745 | assert(host.paramChangeOutputs!(Params.filterQ)(0.5)); 746 | } 747 | } 748 | 749 | host.setParam!(Params.filterCutoff)(0); 750 | assert(host.paramChangeOutputs!(Params.filterTrack)(1.0)); 751 | host.setParam!(Params.filterEnvAmount)(1.0); 752 | assert(host.paramChangeOutputs!(Params.filterUseVelocity)(true)); 753 | assert(host.paramChangeOutputs!(Params.filterAttack)(1.0)); 754 | assert(host.paramChangeOutputs!(Params.saturation)(1.0)); 755 | 756 | // host.frames = 1000; 757 | // assert(host.paramChangeOutputs!(Params.filterDecay)(10.0)); 758 | // assert(host.paramChangeOutputs!(Params.filterSustain)(-10)); 759 | // assert(host.paramChangeOutputs!(Params.filterRelease)(1.0)); 760 | } 761 | 762 | /// Test filter 763 | @nogc nothrow @system 764 | unittest { 765 | TestHost host = { mallocNew!Synth2Client() }; 766 | scope (exit) destroyFree(host.client); 767 | 768 | host.frames = 1000; 769 | host.setParam!(Params.oscMix)(0.5); 770 | host.setParam!(Params.oscSubVol)(0.5); 771 | host.setParam!(Params.osc1Waveform)(Waveform.pulse); 772 | assert(host.paramChangeOutputs!(Params.menvAmount)(1.0)); 773 | host.setParam!(Params.menvAmount)(1.0); 774 | 775 | foreach (dest; EnumMembers!MEnvDest) { 776 | host.setParam!(Params.menvDest)(dest); 777 | assert(host.paramChangeOutputs!(Params.menvAmount)(0.5)); 778 | assert(host.paramChangeOutputs!(Params.menvAttack)(1.0)); 779 | assert(host.paramChangeOutputs!(Params.menvDecay)(1.0)); 780 | } 781 | } 782 | 783 | /// Test effect 784 | @nogc nothrow @system 785 | unittest { 786 | TestHost host = { mallocNew!Synth2Client() }; 787 | scope (exit) destroyFree(host.client); 788 | 789 | host.frames = 1000; 790 | host.setParam!(Params.effectMix)(1.0); 791 | 792 | static immutable kinds = [EnumMembers!EffectKind]; 793 | foreach (EffectKind kind; kinds) { 794 | host.setParam!(Params.effectKind)(kind); 795 | assert(host.paramChangeOutputs!(Params.effectCtrl1)(0.001)); 796 | // assert(host.paramChangeOutputs!(Params.effectCtrl2)(0.1)); 797 | } 798 | } 799 | 800 | /// Test EQ 801 | @nogc nothrow @system 802 | unittest { 803 | TestHost host = { mallocNew!Synth2Client() }; 804 | scope (exit) destroyFree(host.client); 805 | 806 | assert(host.paramChangeOutputs!(Params.eqLevel)(-1)); 807 | assert(host.paramChangeOutputs!(Params.eqPan)(-1)); 808 | assert(host.paramChangeOutputs!(Params.eqTone)(-1)); 809 | assert(host.paramChangeOutputs!(Params.eqTone)(1)); 810 | } 811 | 812 | /// Test LFO 813 | @nogc nothrow @system 814 | unittest { 815 | TestHost host = { mallocNew!Synth2Client() }; 816 | scope (exit) destroyFree(host.client); 817 | 818 | assert(host.paramChangeOutputs!(Params.lfo1Amount)(1)); 819 | assert(host.paramChangeOutputs!(Params.lfo2Amount)(1)); 820 | 821 | host.setParam!(Params.oscMix)(0.5); 822 | host.setParam!(Params.osc1Waveform)(Waveform.pulse); 823 | host.setParam!(Params.lfo1Amount)(1.0); 824 | host.frames = 1000; 825 | host.noteOff = true; 826 | assert(host.paramChangeOutputs!(Params.lfo1Dest)(LfoDest.amp)); 827 | foreach (dest; EnumMembers!LfoDest) { 828 | host.setParam!(Params.lfo1Dest)(dest); 829 | assert(host.paramChangeOutputs!(Params.lfo1Speed)(1)); 830 | assert(host.paramChangeOutputs!(Params.lfo1Wave)(Waveform.sine)); 831 | assert(host.paramChangeOutputs!(Params.lfo1Sync)(false)); 832 | assert(host.paramChangeOutputs!(Params.lfo1Mul)(Multiplier.dot)); 833 | assert(host.paramChangeOutputs!(Params.lfo1Trigger)(true)); 834 | } 835 | } 836 | 837 | /// Test voice 838 | @nogc nothrow @system 839 | unittest { 840 | TestHost host = { mallocNew!Synth2Client() }; 841 | scope (exit) destroyFree(host.client); 842 | 843 | assert(host.paramChangeOutputs!(Params.voiceKind)(VoiceKind.mono)); 844 | assert(host.paramChangeOutputs!(Params.voiceKind)(VoiceKind.legato)); 845 | 846 | host.frames = 1000; 847 | host.setParam!(Params.voiceKind)(VoiceKind.legato); 848 | assert(host.paramChangeOutputs!(Params.voicePortament)(1)); 849 | // TODO: assert(host.paramChangeOutputs!(Params.voicePortamentAuto)(false)); 850 | } 851 | 852 | /// Test Chorus 853 | @nogc nothrow @system 854 | unittest { 855 | TestHost host = { mallocNew!Synth2Client() }; 856 | scope (exit) destroyFree(host.client); 857 | 858 | host.frames = 1000; 859 | // TODO: test On/Off sound diff. 860 | host.setParam!(Params.chorusOn)(true); 861 | host.setParam!(Params.chorusLevel)(1.0); 862 | host.setParam!(Params.chorusMulti)(2); 863 | assert(host.paramChangeOutputs!(Params.chorusMulti)(1)); 864 | assert(host.paramChangeOutputs!(Params.chorusMulti)(3)); 865 | assert(host.paramChangeOutputs!(Params.chorusMulti)(4)); 866 | assert(host.paramChangeOutputs!(Params.chorusTime)(40)); 867 | assert(host.paramChangeOutputs!(Params.chorusDepth)(0.5)); 868 | assert(host.paramChangeOutputs!(Params.chorusRate)(20)); 869 | assert(host.paramChangeOutputs!(Params.chorusFeedback)(1)); 870 | assert(host.paramChangeOutputs!(Params.chorusLevel)(1)); 871 | assert(host.paramChangeOutputs!(Params.chorusWidth)(1)); 872 | } 873 | -------------------------------------------------------------------------------- /source/synth2/delay.d: -------------------------------------------------------------------------------- 1 | module synth2.delay; 2 | 3 | import synth2.ringbuffer : RingBuffer; 4 | 5 | /// Maximum delay interval in seconds. 6 | enum maxDelaySec = 10.0f; 7 | 8 | /// Kind of delay stereo effects. 9 | enum DelayKind { 10 | st, // normal stereo 11 | x, // cross feedback 12 | pp, // pingpong 13 | } 14 | 15 | /// String names of delay kinds. 16 | static immutable delayNames = [__traits(allMembers, DelayKind)]; 17 | 18 | /// Delay effect. 19 | struct Delay { 20 | @nogc nothrow pure: 21 | 22 | void setSampleRate(float sampleRate) { 23 | _sampleRate = sampleRate; 24 | const maxFrames = cast(size_t) (sampleRate * maxDelaySec); 25 | foreach (ref b; _buffers) { 26 | b.recalloc(maxFrames); 27 | } 28 | } 29 | 30 | void setParams(DelayKind kind, float delaySecs, float spread, float feedback) { 31 | _kind = kind; 32 | _feedback = feedback; 33 | const delayFrames = cast(size_t) (delaySecs * _sampleRate); 34 | const spreadFrames = cast(size_t) (spread * _sampleRate); 35 | _buffers[0].resize(delayFrames + spreadFrames); 36 | _buffers[1].resize( 37 | cast(size_t) (delayFrames * (_kind == DelayKind.pp ? 1 : 1 / 1.5))); 38 | } 39 | 40 | /// Applies delay effect. 41 | /// Params: 42 | /// x = dry stereo input. 43 | /// Returns: 44 | /// wet delayed output. 45 | float[2] apply(float[2] x...) { 46 | float[2] y; 47 | y[0] = _buffers[0].front; 48 | y[1] = _buffers[1].front; 49 | size_t f0 = 0; 50 | size_t f1 = 1; 51 | if (_kind == DelayKind.x) { 52 | f0 = 1; 53 | f1 = 0; 54 | } 55 | _buffers[0].enqueue((1f - _feedback) * x[0] + _feedback * y[f0]); 56 | _buffers[1].enqueue((1f - _feedback) * x[1] + _feedback * y[f1]); 57 | return y; 58 | } 59 | 60 | private: 61 | DelayKind _kind; 62 | RingBuffer!float[2] _buffers; 63 | float _feedback = 0; 64 | float _sampleRate = 44_100; 65 | } 66 | 67 | unittest { 68 | Delay dly; 69 | dly.setSampleRate(44_100); 70 | dly.setParams(DelayKind.st, 0.5, 0, 0); 71 | assert(dly.apply(1f, 2f) == [0f, 0f]); 72 | 73 | dly.setParams(DelayKind.pp, 0.5, 0, 0); 74 | assert(dly.apply(1f, 2f) == [0f, 0f]); 75 | 76 | dly.setParams(DelayKind.x, 0.5, 0, 0); 77 | assert(dly.apply(1f, 2f) == [0f, 0f]); 78 | } 79 | -------------------------------------------------------------------------------- /source/synth2/effect.d: -------------------------------------------------------------------------------- 1 | /** 2 | Effect module. 3 | 4 | Copyright: klknn 2021. 5 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module synth2.effect; 8 | 9 | import std.math : tanh, sgn; 10 | import std.traits : EnumMembers; 11 | 12 | import dplug.core.math : convertLinearGainToDecibel, convertDecibelToLinearGain; 13 | import dplug.core.nogc : mallocNew, destroyFree; 14 | import mir.math : powi, exp, fabs, PI, log2, floor; 15 | 16 | import synth2.waveform : Waveform, WaveformRange; 17 | import synth2.filter : Filter, FilterKind, AllPassFilter; 18 | 19 | /// Base effect class. 20 | interface IEffect { 21 | nothrow @nogc: 22 | /// Sets the sample rate from host. 23 | void setSampleRate(float sampleRate); 24 | 25 | /// Sets parameters for the effect, ctrls are in [0, 1]. 26 | void setParams(float ctrl1, float ctrl2); 27 | 28 | /// Applies the effect configured by ctrl1/2. 29 | float apply(float x); 30 | } 31 | 32 | /// Base distortion with LPF. 33 | abstract class BaseDistortion : IEffect { 34 | public: 35 | nothrow @nogc @safe pure: 36 | 37 | override void setSampleRate(float sampleRate) { 38 | _lpf.setSampleRate(sampleRate); 39 | } 40 | 41 | /// Sets parameters for the effect. 42 | /// Params: 43 | /// ctrl1 = distortion gain. 44 | /// ctrl2 = LPF cutoff. 45 | override void setParams(float ctrl1, float ctrl2) { 46 | _gain = ctrl1 * 10; 47 | _lpf.setParams(FilterKind.LP12, ctrl2, 0); 48 | } 49 | 50 | override float apply(float x) { 51 | return _lpf.apply(distort(_gain * x) / 10); 52 | } 53 | 54 | /// Applies distortion. 55 | abstract float distort(float x) const; 56 | private: 57 | float _gain; 58 | Filter _lpf; 59 | } 60 | 61 | class AnalogDistortionV1 : BaseDistortion { 62 | public: 63 | nothrow @nogc @safe pure 64 | override float distort(float x) const { 65 | return fabs(tanh(x)) * 2f - 1f; 66 | } 67 | } 68 | 69 | class AnalogDistortionV2 : BaseDistortion { 70 | public: 71 | nothrow @nogc @safe pure 72 | override float distort(float x) const { 73 | return tanh(x); 74 | } 75 | } 76 | 77 | class DigitalDistortion : BaseDistortion { 78 | public: 79 | nothrow @nogc @safe pure 80 | override float distort(float x) const { 81 | return sgn(x) * (1 - exp(-fabs(x))); 82 | } 83 | } 84 | 85 | class Resampler : IEffect { 86 | public: 87 | nothrow @nogc @safe pure: 88 | 89 | override void setSampleRate(float sampleRate) { 90 | _sampleRate = sampleRate; 91 | _qx = 0; 92 | _frame = 0; 93 | } 94 | 95 | override void setParams(float ctrl1, float ctrl2) { 96 | // Resample into sampleRate / 1000 .. sampleRate 97 | _resampleFrames = cast(int) ((1 - ctrl1) * _sampleRate / 1000) + 1; 98 | // Scale into 1 .. 24bit 99 | _nbit = cast(int) (ctrl2 * 23 + 1); 100 | } 101 | 102 | override float apply(float x) { 103 | if (_frame == 0) { 104 | // Consider better coding, e.g., mu-low? 105 | _qx = floor(x * _nbit) / _nbit; 106 | } 107 | _frame = (_frame + 1) % _resampleFrames; 108 | return _qx; 109 | } 110 | 111 | private: 112 | float _sampleRate; 113 | // ctrls 114 | int _resampleFrames; 115 | int _nbit; 116 | // states 117 | float _qx; 118 | int _frame; 119 | } 120 | 121 | class RingMod : IEffect { 122 | public: 123 | nothrow @nogc @safe: 124 | 125 | pure override void setSampleRate(float sampleRate) { 126 | _wave.sampleRate = sampleRate; 127 | } 128 | 129 | pure override void setParams(float ctrl1, float ctrl2) { 130 | _wave.freq = log2(ctrl1 + 1) * _wave.sampleRate / 10; 131 | } 132 | 133 | override float apply(float x) @system { 134 | scope (exit) _wave.popFront(); 135 | return _wave.front * x; 136 | } 137 | 138 | private: 139 | WaveformRange _wave = { waveform: Waveform.sine }; 140 | } 141 | 142 | class Compressor : IEffect { 143 | public: 144 | nothrow @nogc @safe pure: 145 | 146 | override void setSampleRate(float sampleRate) { 147 | _avg = 0; 148 | } 149 | 150 | override void setParams(float ctrl1, float ctrl2) { 151 | _threshold = ctrl1; // convertLinearGainToDecibel(ctrl1); 152 | _attack = ctrl2; 153 | } 154 | 155 | override float apply(float x) { 156 | const absx = fabs(x); // convertLinearGainToDecibel(fabs(x)); 157 | _avg = (1 - _attack) * absx + _attack * _avg; 158 | if (_threshold < _avg && _threshold < absx) { 159 | return sgn(x) * // convertDecibelToLinearGain 160 | (_threshold + (absx - _threshold) / _ratio); 161 | } 162 | return x; 163 | } 164 | 165 | private: 166 | float _avg; 167 | float _threshold; 168 | float _sampleRate; 169 | float _ratio = 5; 170 | float _attack; 171 | } 172 | 173 | /// Phase effect. WIP TODO: add LFOs. 174 | /// Params: 175 | /// n = number of the phase effects. 176 | /// See_also: 177 | /// https://ccrma.stanford.edu/realsimple/DelayVar/Phasing_First_Order_Allpass_Filters.html 178 | class Phaser(size_t n) : IEffect { 179 | public: 180 | nothrow @nogc: 181 | /// Sets the sample rate from host. 182 | /// Params: 183 | /// sampleRate = sampling rate. 184 | void setSampleRate(float sampleRate) { 185 | foreach (ref f; _filters) { 186 | f.setSampleRate(sampleRate); 187 | } 188 | } 189 | 190 | /// Sets parameters for the effect, ctrls are in [0, 1]. 191 | /// Params: 192 | /// ctrl1 = all-pass filter cutoff. 193 | /// ctrl2 = mix balance btw dry and phase shifted signals. 194 | void setParams(float ctrl1, float ctrl2) { 195 | foreach (ref f; _filters) { 196 | f.g = ctrl1; 197 | } 198 | _mix = ctrl2; 199 | } 200 | 201 | /// Applies the effect configured by ctrl1/2. 202 | /// Params: 203 | /// x = the current mono input. 204 | /// Returns: output with modulated phase. 205 | float apply(float x) { 206 | float y = x; 207 | foreach (ref f; _filters) { 208 | y = f.apply(y); 209 | } 210 | return _mix * y + (1 - _mix) * x; 211 | } 212 | 213 | private: 214 | AllPassFilter[n] _filters; 215 | float _mix; 216 | } 217 | 218 | /// Effect ids to select one in MultiEffect._effect; 219 | enum EffectKind { 220 | ad1, 221 | ad2, 222 | dd, 223 | deci, 224 | ring, 225 | comp, 226 | ph3, 227 | } 228 | 229 | static immutable effectNames = [__traits(allMembers, EffectKind)]; 230 | 231 | /// Multi effect class for the plugin client. 232 | class MultiEffect : IEffect { 233 | public: 234 | nothrow @nogc: 235 | 236 | this() { 237 | _effects[EffectKind.ad1] = mallocNew!AnalogDistortionV1; 238 | _effects[EffectKind.ad2] = mallocNew!AnalogDistortionV2; 239 | _effects[EffectKind.dd] = mallocNew!DigitalDistortion; 240 | _effects[EffectKind.deci] = mallocNew!Resampler; 241 | _effects[EffectKind.comp] = mallocNew!Compressor; 242 | _effects[EffectKind.ring] = mallocNew!RingMod; 243 | _effects[EffectKind.ph3] = mallocNew!(Phaser!3); 244 | } 245 | 246 | ~this() { 247 | foreach (e; _effects) { 248 | destroyFree(e); 249 | } 250 | } 251 | 252 | void setEffectKind(EffectKind kind) { 253 | _current = kind; 254 | } 255 | 256 | override void setSampleRate(float sampleRate) { 257 | foreach (IEffect e; _effects) { 258 | e.setSampleRate(sampleRate); 259 | } 260 | } 261 | 262 | override void setParams(float ctrl1, float ctrl2) { 263 | _effects[_current].setParams(ctrl1, ctrl2); 264 | } 265 | 266 | override float apply(float x) { 267 | return _effects[_current].apply(x); 268 | } 269 | 270 | private: 271 | EffectKind _current; 272 | IEffect[EnumMembers!EffectKind.length] _effects; 273 | } 274 | 275 | @nogc nothrow @system 276 | unittest { 277 | import std.math : isNaN; 278 | 279 | MultiEffect efx = mallocNew!MultiEffect; 280 | scope (exit) destroyFree(efx); 281 | 282 | efx.setSampleRate(44_100); 283 | foreach (e; EnumMembers!EffectKind) { 284 | efx.setEffectKind(e); 285 | efx.setParams(0.5, 0.5); 286 | const y = efx.apply(0); 287 | assert(!y.isNaN); 288 | assert(-1 <= y && y <= 1); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /source/synth2/envelope.d: -------------------------------------------------------------------------------- 1 | /** 2 | Synth2 ADSR envelope module. 3 | 4 | Copyright: klknn 2021. 5 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module synth2.envelope; 8 | 9 | import std.math : isNaN; 10 | 11 | import dplug.client.midi : MidiMessage; 12 | import mir.math.common : fastmath; 13 | 14 | /// Envelope stages. 15 | enum Stage { 16 | attack, 17 | decay, 18 | sustain, 19 | release, 20 | done, 21 | } 22 | 23 | /// Attack, Decay, Sustain, Release. 24 | struct ADSR { 25 | /// Attack time in #frames. 26 | float attackTime = 0; 27 | /// Decay time in #frames. 28 | float decayTime = 0; 29 | /// Sustain level within [0, 1]. 30 | float sustainLevel = 1; 31 | /// Release time in #frames. 32 | float releaseTime = 0; 33 | 34 | @nogc nothrow @safe pure @fastmath: 35 | 36 | /// Triggers the atack stage. 37 | void attack() { 38 | _stage = Stage.attack; 39 | _stageTime = 0; 40 | } 41 | 42 | /// Triggers the release stage. 43 | void release() { 44 | _releaseLevel = this.front; 45 | _stage = Stage.release; 46 | _stageTime = 0; 47 | } 48 | 49 | void setSampleRate(float sampleRate) { 50 | _frameWidth = 1f / sampleRate; 51 | _stage = Stage.done; 52 | _stageTime = 0; 53 | _nplay = 0; 54 | } 55 | 56 | /// Returns: true if envelope was ended. 57 | bool empty() const { return _stage == Stage.done; } 58 | 59 | /// Returns: an amplitude of the linear envelope. 60 | float front() const { 61 | final switch (_stage) { 62 | case Stage.attack: 63 | return this.attackTime == 0 ? 1 : (_stageTime / this.attackTime); 64 | case Stage.decay: 65 | return this.decayTime == 0 66 | ? 1 : (_stageTime * (this.sustainLevel - 1f) / this.decayTime + 1f); 67 | case Stage.sustain: 68 | return this.sustainLevel; 69 | case Stage.release: 70 | assert(!isNaN(_releaseLevel), "invalid release level."); 71 | return this.releaseTime == 0 ? 0f 72 | : (-_stageTime * _releaseLevel / this.releaseTime 73 | + _releaseLevel); 74 | case Stage.done: 75 | return 0f; 76 | } 77 | } 78 | 79 | /// Update status if the stage is in (attack, decay, release). 80 | void popFront() { 81 | final switch (_stage) { 82 | case Stage.attack: 83 | _stageTime += _frameWidth; 84 | if (_stageTime >= this.attackTime) { 85 | _stage = Stage.decay; 86 | _stageTime = 0; 87 | } 88 | return; 89 | case Stage.decay: 90 | _stageTime += _frameWidth; 91 | if (_stageTime >= this.decayTime) { 92 | _stage = Stage.sustain; 93 | _stageTime = 0; 94 | } 95 | return; 96 | case Stage.sustain: 97 | return; // do nothing. 98 | case Stage.release: 99 | _stageTime += _frameWidth; 100 | if (_stageTime >= this.releaseTime) { 101 | _stage = Stage.done; 102 | _stageTime = 0; 103 | } 104 | return; 105 | case Stage.done: 106 | return; // do nothing. 107 | } 108 | } 109 | 110 | @system void setMidi(MidiMessage msg) { 111 | if (msg.isNoteOn) { 112 | if (_nplay == 0) this.attack(); 113 | ++_nplay; 114 | } 115 | if (msg.isNoteOff) { 116 | --_nplay; 117 | if (_nplay == 0) this.release(); 118 | } 119 | } 120 | 121 | private: 122 | Stage _stage = Stage.done; 123 | float _frameWidth = 1.0 / 44_100; 124 | float _stageTime = 0; 125 | float _releaseLevel; 126 | int _nplay = 0; 127 | } 128 | 129 | /// Test ADSR. 130 | @nogc nothrow pure @safe 131 | unittest { 132 | ADSR env; 133 | env.attackTime = 5; 134 | env.decayTime = 5; 135 | env.sustainLevel = 0.5; 136 | env.releaseTime = 20; 137 | env._frameWidth = 1; 138 | 139 | foreach (_; 0 .. 2) { 140 | env.attack(); 141 | foreach (i; 0 .. env.attackTime) { 142 | assert(env._stage == Stage.attack); 143 | env.popFront(); 144 | } 145 | foreach (i; 0 .. env.decayTime) { 146 | assert(env._stage == Stage.decay); 147 | env.popFront(); 148 | } 149 | assert(env._stage == Stage.sustain); 150 | env.release(); 151 | // foreach does not mutate `env`. 152 | foreach (amp; env) { 153 | assert(env._stage == Stage.release); 154 | } 155 | foreach (i; 0 .. env.releaseTime) { 156 | assert(env._stage == Stage.release); 157 | env.popFront(); 158 | } 159 | assert(env._stage == Stage.done); 160 | assert(env.empty); 161 | assert(env.front == 0); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /source/synth2/equalizer.d: -------------------------------------------------------------------------------- 1 | /** 2 | Synth2 equalizer. 3 | 4 | Copyright: klknn 2021. 5 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module synth2.equalizer; 8 | 9 | import mir.math.common : fabs, log, fmax, exp; 10 | 11 | import synth2.filter : Filter, FilterKind; 12 | 13 | private enum bias = 1e-6; 14 | 15 | /// tone [0, 1] -> [bias, 1] via log curve 16 | private float logTransform(float x) @nogc nothrow pure @safe { 17 | return exp(-(x + bias) * log(bias)) * bias; 18 | } 19 | 20 | /// Equalizer. 21 | struct Equalizer { 22 | @nogc nothrow pure @safe: 23 | 24 | void setSampleRate(float sampleRate) { 25 | _bs.setSampleRate(sampleRate); 26 | _hp.setSampleRate(sampleRate); 27 | _lp.setSampleRate(sampleRate); 28 | } 29 | 30 | void setParams(float level, float freq, float q, float tone) { 31 | _level = level < 0 ? level : 10 * level; 32 | _bs.setParams(FilterKind.BP12, freq, q); 33 | _tone = tone; 34 | if (tone > 0) { 35 | _hp.setParams(FilterKind.HP12, logTransform(tone), 0); 36 | } 37 | if (tone < 0) { 38 | _lp.setParams(FilterKind.LP12, logTransform(1 + tone), 0); 39 | } 40 | } 41 | 42 | /// Applies equalizer. 43 | /// Params: 44 | /// x = input wave frame. 45 | /// Returns: equalized output wave frame. 46 | float apply(float x) { 47 | if (_level != 0) { 48 | x += _level * _bs.apply(x); 49 | } 50 | if (_tone > 0) { 51 | return _hp.apply(x); 52 | } 53 | if (_tone < 0) { 54 | return _lp.apply(x); 55 | } 56 | return x; 57 | } 58 | 59 | private: 60 | float _level = 0; 61 | float _tone = 0; 62 | Filter _bs, _hp, _lp; 63 | } 64 | -------------------------------------------------------------------------------- /source/synth2/filter.d: -------------------------------------------------------------------------------- 1 | /** 2 | Synth2 filters. 3 | 4 | Filter coeffs are generated by tools/filter_coeff.py 5 | For transfer function definitions, 6 | See_also https://www.discodsp.net/VAFilterDesign_2.1.0.pdf 7 | 8 | Copyright: klknn 2021. 9 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 10 | */ 11 | module synth2.filter; 12 | 13 | import mir.math : approxEqual, PI, SQRT2, fmax; 14 | 15 | @nogc nothrow @safe pure: 16 | 17 | /// Kinds of filter implentations. 18 | enum FilterKind { 19 | HP6, 20 | HP12, 21 | BP12, 22 | LP6, 23 | LP12, 24 | /// Moog ladder filter 25 | LP24, 26 | /// TB303 diode-ladder filter 27 | LPDL, 28 | } 29 | 30 | /// String names of filter implementations. 31 | static immutable filterNames = [__traits(allMembers, FilterKind)]; 32 | 33 | /// 34 | struct Filter { 35 | @nogc nothrow @safe pure: 36 | 37 | /// Applies filtering. 38 | /// Params: 39 | /// input = input wave frame. 40 | /// Returns: filtered wave frame. 41 | float apply(float input) { 42 | // TODO: use ring buffer 43 | foreach_reverse (i; 1 .. nFIR) { 44 | x[i] = x[i - 1]; 45 | } 46 | x[0] = input; 47 | 48 | float output = 0; 49 | foreach (i; 0 .. nFIR) { 50 | output += b[i] * x[i]; 51 | } 52 | foreach (i; 0 .. nIIR) { 53 | output -= a[i] * y[i]; 54 | } 55 | 56 | foreach_reverse (i; 1 .. nIIR) { 57 | y[i] = y[i - 1]; 58 | } 59 | y[0] = output; 60 | return output; 61 | } 62 | 63 | void setSampleRate(float sampleRate) { 64 | sampleRate = sampleRate; 65 | x[] = 0f; 66 | y[] = 0f; 67 | } 68 | 69 | /// Set filter parameters. 70 | /// Params: 71 | /// kind = filter type. 72 | /// freq = cutoff frequency [0, 1]. 73 | /// q = resonance, quality factor [0, 1]. 74 | void setParams(FilterKind kind, float freq, float q) { 75 | if (this.kind != kind) { 76 | x[] = 0f; 77 | y[] = 0f; 78 | } 79 | this.kind = kind; 80 | 81 | // To prevent the filter gets unstable. 82 | float Q; 83 | if (kind == FilterKind.LPDL) { 84 | // unstable at Q = 16 (see VAFD sec 5.10) 85 | Q = q * 15; 86 | freq += 0.005; // to prevent self osc. 87 | } 88 | else if (kind == FilterKind.LP24) { 89 | // unstable at Q = 4 (see VAFD sec 5.1, eq 5.2) 90 | Q = q * 3; 91 | freq += 0.005; // to prevent self osc. 92 | } 93 | else { 94 | Q = q * 5 + 1 / SQRT2; 95 | } 96 | const T = 1 / sampleRate; 97 | const w0 = 2 * PI * freq * sampleRate; 98 | assert(T != float.nan); 99 | assert(w0 != float.nan); 100 | final switch (kind) { 101 | mixin(import("filter_coeff.d")); 102 | } 103 | } 104 | 105 | private: 106 | FilterKind kind = FilterKind.LP12; 107 | float sampleRate = 44_100; 108 | // filter and prev inputs 109 | float[5] b, x; 110 | // filter and prev outputs 111 | float[4] a, y; 112 | 113 | int nFIR = 3; 114 | int nIIR = 2; 115 | } 116 | 117 | unittest { 118 | Filter f; 119 | f.setSampleRate(20); 120 | f.setParams(FilterKind.LP12, 5, 2); 121 | 122 | // with padding 123 | auto y0 = f.apply(0.1); 124 | assert(approxEqual(y0, f.b[0] * 0.1)); 125 | 126 | auto y1 = f.apply(0.2); 127 | assert(approxEqual(y1, f.b[0] * 0.2 + f.b[1] * 0.1 - f.a[0] * y0)); 128 | 129 | auto y2 = f.apply(0.3); 130 | assert(approxEqual(y2, 131 | f.b[0] * 0.3 + f.b[1] * 0.2 + f.b[0] * 0.1 132 | -f.a[0] * y1 - f.a[1] * y0)); 133 | 134 | // without padding 135 | auto y3 = f.apply(0.4); 136 | assert(approxEqual(y3, 137 | f.b[0] * 0.4 + f.b[1] * 0.3 + f.b[0] * 0.2 138 | -f.a[0] * y2 - f.a[1] * y1)); 139 | } 140 | 141 | /// Single frame delayed all pass filter. 142 | struct AllPassFilter { 143 | @nogc nothrow pure @safe: 144 | 145 | float g = 0.5, py = 0, px = 0; 146 | 147 | void setSampleRate(float) { 148 | py = 0; 149 | px = 0; 150 | } 151 | 152 | float apply(float x) { 153 | const y = g * x + px - g * py; 154 | px = x; 155 | py = y; 156 | return y; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /source/synth2/gui.d: -------------------------------------------------------------------------------- 1 | /** 2 | Synth2 graphical user interface. 3 | 4 | Copyright: klknn, 2021. 5 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module synth2.gui; 8 | 9 | import core.stdc.stdio : snprintf; 10 | import std.algorithm : max; 11 | 12 | import dplug.client.params : BoolParameter, FloatParameter, IntegerParameter, Parameter, IParameterListener; 13 | import dplug.core : mallocNew, makeVec, destroyFree, Vec; 14 | import dplug.graphics.color : RGBA; 15 | import dplug.graphics.font : Font; 16 | import dplug.gui : UIElement; 17 | import dplug.flatwidgets : makeSizeConstraintsDiscrete, UIWindowResizer; 18 | import dplug.pbrwidgets : PBRBackgroundGUI, UILabel, UIOnOffSwitch, UIKnob, UISlider, KnobStyle, HandleStyle; 19 | import dplug.math : box2i, rectangle; 20 | 21 | import synth2.lfo : multiplierNames, mulToFloat, Multiplier; 22 | import synth2.delay : delayNames; 23 | import synth2.effect : effectNames; 24 | import synth2.filter : filterNames; 25 | import synth2.params : typedParam, Params, menvDestNames, lfoDestNames, voiceKindNames, maxPoly; 26 | 27 | // TODO: CTFE formatted names from enum values. 28 | private static immutable mulNames = { 29 | import std.traits : EnumMembers; 30 | import std.format : format; 31 | import std.conv : to; 32 | string[] ret; 33 | foreach (mul; mulToFloat) { 34 | ret ~= format!"%d.%d"(cast(int) mul, cast(int) ((mul % 1) * 10)); 35 | } 36 | return ret; 37 | }(); 38 | 39 | nothrow @nogc pure @safe 40 | unittest { 41 | assert(mulNames[Multiplier.none] == "1.0"); 42 | assert(mulNames[Multiplier.dot] == "1.5"); 43 | assert(mulNames[Multiplier.tri] == "0.3"); 44 | } 45 | 46 | private enum png1 = "114.png"; // "gray.png"; // "black.png" 47 | private enum png2 = "black.png"; 48 | private enum png3 = "black.png"; 49 | 50 | 51 | // https://all-free-download.com/font/download/display_free_tfb_10784.html 52 | // static string _fontRaw = import("TFB.ttf"); 53 | // http://www.publicdomainfiles.com/show_file.php?id=13502494517207 54 | // static string _fontRaw = import("LeroyLetteringLightBeta01.ttf"); 55 | // https://www.google.com/get/noto/#mono-mono 56 | // static string _fontRaw = import("NotoMono-Regular.ttf"); 57 | // https://all-free-download.com/font/download/forced_square_14817.html 58 | private static string _fontRaw = import("FORCED SQUARE.ttf"); 59 | 60 | /// Expands box to include all positions. 61 | private box2i expand(box2i[] positions...) nothrow @nogc pure @safe { 62 | box2i ret = positions[0]; 63 | foreach (p; positions) { 64 | ret = ret.expand(p); 65 | } 66 | return ret; 67 | } 68 | 69 | nothrow @nogc pure 70 | unittest { 71 | auto a = rectangle(1, 10, 3, 4); 72 | auto b = rectangle(100, 2, 3, 4); 73 | assert(expand(a, a, b) == box2i(1, 2, 103, 14)); 74 | } 75 | 76 | /// width getter 77 | @nogc nothrow 78 | private auto width(UIElement label) { 79 | return label.position.width; 80 | } 81 | 82 | /// width setter 83 | @nogc nothrow 84 | private void width(UIElement label, int width) { 85 | auto p = label.position; 86 | p.width(width); 87 | label.position(p); 88 | } 89 | 90 | /// 91 | unittest { 92 | auto label = new UILabel(null, null); 93 | assert(label.width == 0); 94 | label.width = 1; 95 | assert(label.width == 1); 96 | } 97 | 98 | version (unittest) {} else: 99 | 100 | /// GUI class. 101 | class Synth2GUI : PBRBackgroundGUI!(png1, png2, png3, png3, png3, ""), IParameterListener { 102 | public: 103 | nothrow @nogc: 104 | 105 | /// 106 | this(Parameter[] parameters) { 107 | setUpdateMargin(0); 108 | 109 | _params = parameters; 110 | _font = mallocNew!Font(cast(ubyte[])(_fontRaw)); 111 | 112 | _params[Params.voicePoly].addListener(this); 113 | _params[Params.chorusMulti].addListener(this); 114 | 115 | static immutable float[7] ratios = [0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f]; 116 | super(makeSizeConstraintsDiscrete(screenWidth, screenHeight, ratios)); 117 | int y; 118 | 119 | // header 120 | y = marginH; 121 | _synth2 = _addLabel("Synth2", 0, marginH, fontLarge); 122 | _date = _addLabel("v0.00 " ~ __DATE__ ~ __TIME__, _synth2.position.max.x + marginW, 123 | _synth2.position.min.y, fontMedium); 124 | _tempo = _addLabel("BPM000.0", _date.position.max.x + marginW, 125 | _synth2.position.min.y, fontMedium); 126 | 127 | enum marginWSec = marginW * 5; 128 | 129 | const osc = _buildOsc(marginW, _synth2.position.max.y + marginH); 130 | 131 | const master = _buildMaster(osc.max.x + marginWSec, osc.min.y); 132 | 133 | const menv = _buildModEnv(master.min.x, master.max.y + marginH * 3); 134 | 135 | const ampEnv = _buildADSR(master.max.x + marginWSec, osc.min.y, "AmpEnv", 136 | Params.ampAttack); 137 | 138 | // const filterEnv = 139 | _buildADSR(ampEnv.min.x, ampEnv.max.y, 140 | "FilterEnv", Params.filterAttack); 141 | 142 | const filter = _buildFilter(menv.max.x + marginWSec, menv.min.y); 143 | 144 | const effect = _buildEffect(ampEnv.max.x + marginWSec, ampEnv.min.y); 145 | 146 | // const eq = 147 | _buildEQ(effect.max.x + marginWSec, effect.min.y); 148 | 149 | const delay = _buildDelay(filter.max.x + marginWSec, effect.max.y + marginH * 3); 150 | 151 | // const chorus = 152 | _buildChorus(delay.max.x + marginWSec, delay.min.y); 153 | 154 | const lfo1 = _buildLFO!(cast(Params) 0)( 155 | "LFO1", osc.min.x, osc.max.y + marginH); 156 | 157 | const lfo2 = _buildLFO!(Params.lfo2Dest - Params.lfo1Dest)( 158 | "LFO2", lfo1.max.x + marginWSec, lfo1.min.y); 159 | 160 | // const voice = 161 | _buildVoice(lfo2.max.x + marginWSec, lfo2.min.y); 162 | 163 | addChild(_resizerHint = mallocNew!UIWindowResizer(this.context())); 164 | 165 | _defaultRects = makeVec!box2i(_children.length); 166 | _defaultTextSize = makeVec!float(_children.length); 167 | foreach (i, child; _children) { 168 | _defaultRects[i] = child.position; 169 | if (auto label = cast(UILabel) child) { 170 | _defaultTextSize[i] = label.textSize(); 171 | } 172 | } 173 | } 174 | 175 | ~this() { 176 | _font.destroyFree(); 177 | } 178 | 179 | override void reflow() { 180 | super.reflow(); 181 | 182 | const int W = position.width; 183 | const int H = position.height; 184 | float S = W / cast(float)(context.getDefaultUIWidth()); 185 | foreach (i, child; _children) { 186 | child.position(_defaultRects[i].scaleByFactor(S)); 187 | if (auto label = cast(UILabel) child) { 188 | label.textSize(_defaultTextSize[i] * S); 189 | } 190 | } 191 | enum hintSize = 20; 192 | _resizerHint.position = rectangle(W - hintSize, H - hintSize, 193 | hintSize, hintSize); 194 | } 195 | 196 | void setTempo(double tempo) { 197 | if (_tempoValue == tempo) return; 198 | snprintf(_tempoStr.ptr, _tempoStr.length, "BPM%3.1lf", tempo); 199 | _tempo.text(cast(string) _tempoStr[]); 200 | _tempoValue = tempo; 201 | } 202 | 203 | void setPoly(int poly) { 204 | snprintf(_polyStr.ptr, _polyStr.length, "%02d", poly); 205 | _poly.text(cast(string) _polyStr[]); 206 | } 207 | 208 | void setChorusMulti(int multi) { 209 | snprintf(_chorusMultiStr.ptr, _chorusMultiStr.length, "%d", multi); 210 | _chorusMulti.text(cast(string) _chorusMultiStr[]); 211 | } 212 | 213 | /// Listens to parameter sender. 214 | /// TODO: create a new UILabel with IParameterListner for IntegerParameter. 215 | void onParameterChanged(Parameter sender) { 216 | if (sender.index == Params.voicePoly) { 217 | if (auto polyParam = cast(IntegerParameter) sender) { 218 | setPoly(polyParam.value); 219 | } 220 | } 221 | if (sender.index == Params.chorusMulti) { 222 | if (auto polyParam = cast(IntegerParameter) sender) { 223 | setChorusMulti(polyParam.value); 224 | } 225 | } 226 | } 227 | 228 | /// 229 | void onBeginParameterEdit(Parameter) {} 230 | 231 | /// 232 | void onEndParameterEdit(Parameter) {} 233 | 234 | private: 235 | 236 | auto _param(Params id)() { return typedParam!id(_params); } 237 | 238 | box2i _buildChorus(int x, int y) { 239 | auto label = _addLabel("Chorus", x, y, fontMedium); 240 | auto on = _buildSwitch( 241 | _param!(Params.chorusOn), 242 | rectangle(x, label.position.max.y + marginH, knobRad, knobRad), 243 | "ON"); 244 | 245 | auto multi = _buildSlider( 246 | _param!(Params.chorusMulti), 247 | rectangle(on.max.x + marginW, on.min.y, slideWidth, slideHeight / 3), 248 | "", []); 249 | _chorusMulti = _addLabel("1", multi.max.x, multi.min.y, fontLarge); 250 | auto multiLabel = _addLabel("multi", 251 | _chorusMulti.position.min.x, 252 | _chorusMulti.position.max.y + marginH, 253 | fontSmall); 254 | _chorusMulti.width = multiLabel.width; 255 | 256 | auto chorusTime = _buildKnob( 257 | _param!(Params.chorusTime), 258 | rectangle(x, on.max.y + marginH, knobRad, knobRad), 259 | "time"); 260 | auto chorusDepth = _buildKnob( 261 | _param!(Params.chorusDepth), 262 | rectangle(chorusTime.max.x + marginW, 263 | on.max.y + marginH, knobRad, knobRad), 264 | "deph"); 265 | auto chorusRate = _buildKnob( 266 | _param!(Params.chorusRate), 267 | rectangle(chorusDepth.max.x + marginH, 268 | on.max.y + marginH, knobRad, knobRad), 269 | "rate"); 270 | 271 | auto chorusFeedback = _buildKnob( 272 | _param!(Params.chorusFeedback), 273 | rectangle(x, chorusTime.max.y + marginH, knobRad, knobRad), 274 | "fdbk"); 275 | auto chorusLevel = _buildKnob( 276 | _param!(Params.chorusLevel), 277 | rectangle(chorusFeedback.max.x + marginW, 278 | chorusTime.max.y + marginH, knobRad, knobRad), 279 | "levl"); 280 | auto chorusWidth = _buildKnob( 281 | _param!(Params.chorusWidth), 282 | rectangle(chorusLevel.max.x + marginW, 283 | chorusTime.max.y + marginH, knobRad, knobRad), 284 | "widh"); 285 | return expand(label.position, on, 286 | _chorusMulti.position, multiLabel.position, 287 | chorusTime, chorusDepth, chorusRate, 288 | chorusFeedback, chorusLevel, chorusWidth); 289 | } 290 | 291 | box2i _buildDelay(int x, int y) { 292 | auto label = _addLabel("Delay", x, y, fontMedium); 293 | auto kind = _buildSlider( 294 | _param!(Params.delayKind), 295 | rectangle(x, label.position.max.y + marginW, slideWidth, slideHeight * 3 / 5), 296 | "kind", delayNames); 297 | auto mul = _buildSlider( 298 | _param!(Params.delayMul), 299 | rectangle(kind.max.x + marginW, kind.min.y, slideWidth, slideHeight * 3 / 5), 300 | "note", mulNames); 301 | 302 | auto spread = _buildKnob( 303 | _param!(Params.delaySpread), 304 | rectangle(mul.max.x + marginW, mul.min.y, knobRad, knobRad), "sprd"); 305 | auto feedback = _buildKnob( 306 | _param!(Params.delayFeedback), 307 | rectangle(spread.min.x, spread.max.y + marginH, knobRad, knobRad), "fdbk"); 308 | auto tone = _buildKnob( 309 | _param!(Params.delayTone), 310 | rectangle(spread.min.x, feedback.max.y + marginH, knobRad, knobRad), "tone"); 311 | 312 | auto mix = _buildKnob( 313 | _param!(Params.delayMix), 314 | rectangle(x, tone.min.y, knobRad, knobRad), "mix"); 315 | auto time = _buildKnob( 316 | _param!(Params.delayTime), 317 | rectangle(mul.min.x, tone.min.y, knobRad, knobRad), "time"); 318 | 319 | return expand(label.position, kind, time, mul, mix, spread, feedback, tone); 320 | } 321 | 322 | /// Builds the Voice section. 323 | box2i _buildVoice(int x, int y) { 324 | auto label = _addLabel("Voice", x, y, fontMedium); 325 | auto kind = _buildSlider( 326 | _params[Params.voiceKind], 327 | rectangle(x, label.position.max.y + marginH, slideWidth, slideHeight / 3), 328 | "", voiceKindNames); 329 | auto poly = _buildSlider( 330 | _param!(Params.voicePoly), 331 | rectangle(x, kind.max.y + marginH, slideWidth, slideHeight / 3), 332 | "", []); 333 | const polyWidth = kind.width - poly.width; 334 | _poly = _addLabel(maxPoly.stringof, poly.max.x, poly.min.y, fontLarge); 335 | _poly.width = polyWidth; 336 | auto polyLabel = _addLabel("voices", 337 | _poly.position.min.x, 338 | _poly.position.max.y + marginH, 339 | fontSmall); 340 | polyLabel.width = polyWidth; 341 | auto port = _buildKnob( 342 | typedParam!(Params.voicePortament)(_params), 343 | rectangle(x, poly.max.y + marginH, knobRad, knobRad), "port"); 344 | // const portAuto = 345 | _buildSwitch( 346 | _param!(Params.voicePortamentAuto), 347 | rectangle(port.max.x + marginW, port.min.y, knobRad, knobRad), 348 | "auto"); 349 | return expand(label.position, kind, port, _poly.position); 350 | } 351 | 352 | /// Builds the Master section. 353 | box2i _buildMaster(int x, int y) { 354 | auto oscMaster = this._addLabel("Master", x, y, fontMedium); 355 | auto oscKeyShift = this._buildSlider( 356 | _params[Params.oscKeyShift], 357 | rectangle(oscMaster.position.min.x, oscMaster.position.max.y + marginH, 358 | slideWidth, slideHeight), 359 | "pitch", pitchLabels); 360 | auto oscMasterMix = this._buildKnob( 361 | typedParam!(Params.oscMix)(_params), 362 | rectangle(oscKeyShift.max.x + marginW, oscKeyShift.min.y, 363 | knobRad, knobRad), 364 | "mix", 365 | ); 366 | auto oscMasterPhase = this._buildKnob( 367 | typedParam!(Params.oscPhase)(_params), 368 | rectangle(oscMasterMix.min.x, oscMasterMix.max.y + marginH, knobRad, knobRad), 369 | "phase", 370 | ); 371 | auto oscMasterPW = this._buildKnob( 372 | typedParam!(Params.oscPulseWidth)(_params), 373 | rectangle(oscMasterMix.max.x + marginW, oscMasterMix.min.y, knobRad, knobRad), 374 | "p/w", 375 | ); 376 | auto oscMasterTune = this._buildKnob( 377 | typedParam!(Params.oscTune)(_params), 378 | rectangle(oscMasterPW.min.x, oscMasterPhase.min.y, knobRad, knobRad), 379 | "tune", 380 | ); 381 | 382 | // Amplifier 383 | auto ampGain = this._buildKnob( 384 | typedParam!(Params.ampGain)(_params), 385 | rectangle(oscMasterPhase.min.x, oscMasterPhase.max.y + marginH, 386 | knobRad, knobRad), 387 | "gain", 388 | ); 389 | auto ampVel = this._buildKnob( 390 | typedParam!(Params.ampVel)(_params), 391 | rectangle(oscMasterTune.min.x, oscMasterTune.max.y + marginH, 392 | knobRad, knobRad), 393 | "vel", 394 | ); 395 | return expand(oscMaster.position, oscMasterMix, 396 | oscKeyShift, oscMasterPhase, 397 | oscMasterPW, oscMasterTune, 398 | ampGain, ampVel); 399 | } 400 | 401 | /// Builds the Osc section. 402 | box2i _buildOsc(int x, int y) { 403 | // osc1 404 | auto osc1lab = this._addLabel("Osc1", x, y, fontMedium); 405 | auto osc1wave = this._buildSlider( 406 | _params[Params.osc1Waveform], 407 | rectangle(osc1lab.position.min.x, osc1lab.position.max.y + marginH, 408 | slideWidth, slideHeight), 409 | "wave", 410 | waveNames, 411 | ); 412 | auto osc1det = this._buildKnob( 413 | cast(FloatParameter) _params[Params.osc1Det], 414 | rectangle(osc1wave.max.x + marginW, osc1wave.min.y, knobRad, knobRad), 415 | "det"); 416 | auto osc1fm = this._buildKnob( 417 | cast(FloatParameter) _params[Params.osc1FM], 418 | rectangle(osc1det.min.x, osc1det.max.y + marginH, 419 | knobRad, knobRad), 420 | "fm"); 421 | 422 | // oscSub 423 | auto oscSublab = this._addLabel( 424 | "OscSub", osc1det.max.x, osc1lab.position.min.y, fontMedium); 425 | auto oscSubwave = this._buildSlider( 426 | _params[Params.oscSubWaveform], 427 | rectangle(oscSublab.position.min.x + marginW, osc1wave.min.y, 428 | slideWidth, slideHeight), 429 | "wave", waveNames); 430 | auto oscSubVol = this._buildKnob( 431 | cast(FloatParameter) _params[Params.oscSubVol], 432 | rectangle(oscSubwave.max.x + marginW, oscSubwave.min.y, knobRad, knobRad), 433 | "vol "); 434 | auto oscSubOct = this._buildSwitch( 435 | typedParam!(Params.oscSubOct)(_params), 436 | rectangle(oscSubVol.min.x, osc1fm.min.y, knobRad, knobRad), 437 | "-1oct" 438 | ); 439 | 440 | // osc2 441 | auto osc2lab = this._addLabel( 442 | "Osc2", osc1lab.position.min.x, osc1wave.max.y + marginH * 3, fontMedium); 443 | auto osc2wave = this._buildSlider( 444 | _params[Params.osc2Waveform], 445 | rectangle(osc1wave.min.x, osc2lab.position.max.y + marginW, 446 | slideWidth, slideHeight), 447 | "wave", 448 | waveNames, 449 | ); 450 | auto osc2ring = this._buildSwitch( 451 | typedParam!(Params.osc2Ring)(_params), 452 | rectangle(osc1det.min.x, osc2wave.min.y, knobRad, knobRad), 453 | "ring" 454 | ); 455 | auto osc2sync = this._buildSwitch( 456 | typedParam!(Params.osc2Sync)(_params), 457 | rectangle(osc2ring.min.x, osc2ring.max.y + marginH, knobRad, knobRad), 458 | "sync" 459 | ); 460 | auto osc2pitch = this._buildSlider( 461 | _params[Params.osc2Pitch], 462 | rectangle(oscSubwave.min.x, osc2ring.min.y, slideWidth, slideHeight), 463 | "pitch", pitchLabels); 464 | auto osc2tune = this._buildKnob( 465 | cast(FloatParameter) _params[Params.osc2Fine], 466 | rectangle(oscSubVol.min.x, osc2ring.min.y, knobRad, knobRad), 467 | "tune"); 468 | auto osc2track = this._buildSwitch( 469 | typedParam!(Params.osc2Track)(_params), 470 | rectangle(osc2tune.min.x, osc2sync.min.y, knobRad, knobRad), 471 | "track" 472 | ); 473 | return expand(osc1lab.position, osc1wave, osc1det, osc1fm, 474 | osc2lab.position, osc2wave, osc2ring, osc2sync, 475 | osc2pitch, osc2tune, osc2track, 476 | oscSublab.position, oscSubwave, oscSubVol, oscSubOct); 477 | } 478 | 479 | /// Builds the Filter section. 480 | box2i _buildFilter(int x, int y) { 481 | auto filterLab = this._addLabel("Filter", x, y, fontMedium); 482 | auto filterKind = this._buildSlider( 483 | _params[Params.filterKind], 484 | rectangle(x, y + fontMedium + marginH, slideWidth, slideHeight), 485 | "type", filterNames); 486 | auto filterCutoff = this._buildKnob( 487 | typedParam!(Params.filterCutoff)(_params), 488 | rectangle(filterKind.max.x + marginW, filterKind.min.y, 489 | knobRad, knobRad), 490 | "frq"); 491 | auto filterQ = this._buildKnob( 492 | typedParam!(Params.filterQ)(_params), 493 | rectangle(filterCutoff.min.x, filterCutoff.max.y + marginH, 494 | knobRad, knobRad), 495 | "res"); 496 | auto saturation = this._buildKnob( 497 | typedParam!(Params.saturation)(_params), 498 | rectangle(filterCutoff.min.x, filterQ.max.y + marginH, 499 | knobRad, knobRad), 500 | "sat"); 501 | auto filterEnvAmount = this._buildKnob( 502 | typedParam!(Params.filterEnvAmount)(_params), 503 | rectangle(filterCutoff.max.x + marginW, filterKind.min.y, 504 | knobRad, knobRad), 505 | "amt"); 506 | auto filterTrack = this._buildKnob( 507 | typedParam!(Params.filterTrack)(_params), 508 | rectangle(filterEnvAmount.min.x, filterQ.min.y, 509 | knobRad, knobRad), 510 | "track"); 511 | auto filterVel = this._buildSwitch( 512 | typedParam!(Params.filterUseVelocity)(_params), 513 | rectangle(filterTrack.min.x, filterTrack.max.y + marginH, 514 | knobRad, knobRad), 515 | "vel" 516 | ); 517 | return expand(filterLab.position, filterKind, filterCutoff, filterQ, 518 | saturation, filterEnvAmount, filterTrack, filterVel); 519 | } 520 | 521 | /// Builds a ADSR section. Assumes params for ADSR are contiguous. 522 | box2i _buildADSR(int x, int y, string label, Params attack) { 523 | auto EnvLab = this._addLabel(label, x, y, fontMedium); 524 | enum height = cast(int) (slideHeight * 2f / 5); 525 | auto A = this._buildSlider( 526 | _params[attack], 527 | rectangle(x, EnvLab.position.max.y + marginH, slideWidth, height), 528 | "A", [] 529 | ); 530 | auto D = this._buildSlider( 531 | _params[attack + 1], 532 | rectangle(A.max.x + marginW, A.min.y, slideWidth, height), 533 | "D", [] 534 | ); 535 | auto S = this._buildSlider( 536 | _params[attack + 2], 537 | rectangle(D.max.x + marginW, A.min.y, slideWidth, height), 538 | "S", [] 539 | ); 540 | auto R = this._buildSlider( 541 | _params[attack + 3], 542 | rectangle(S.max.x + marginW, A.min.y, slideWidth, height), 543 | "R", [] 544 | ); 545 | return expand(EnvLab.position, A, D, S, R); 546 | } 547 | 548 | /// Build "ModEnv" section. 549 | box2i _buildModEnv(int x, int y) { 550 | auto menvLabel = this._addLabel("ModEnv", x, y, fontMedium); 551 | auto menvDest = this._buildSlider( 552 | _params[Params.menvDest], 553 | rectangle( 554 | menvLabel.position.min.x, 555 | menvLabel.position.max.y + marginH, 556 | slideWidth, slideHeight, 557 | ), "dst", menvDestNames); 558 | auto menvAmount = this._buildKnob( 559 | typedParam!(Params.menvAmount)(_params), 560 | rectangle(menvDest.max.x + marginW, menvDest.min.y, knobRad, knobRad), 561 | "amt"); 562 | auto menvAttack = this._buildKnob( 563 | typedParam!(Params.menvAttack)(_params), 564 | rectangle(menvAmount.min.x, menvAmount.max.y + marginH, knobRad, knobRad), 565 | "A"); 566 | auto menvDecay = this._buildKnob( 567 | typedParam!(Params.menvDecay)(_params), 568 | rectangle(menvAmount.min.x, menvAttack.max.y + marginH, 569 | knobRad, knobRad), 570 | "D"); 571 | return expand(menvLabel.position, menvDest, 572 | menvAmount, menvAttack, menvDecay); 573 | } 574 | 575 | /// Build "Effect" section. 576 | box2i _buildEffect(int x, int y) { 577 | auto effectLabel = this._addLabel("Effect", x, y, fontMedium); 578 | auto effectKind = this._buildSlider( 579 | _params[Params.effectKind], 580 | rectangle(effectLabel.position.min.x, effectLabel.position.max.y + marginH, 581 | slideWidth, slideHeight), 582 | "kind", effectNames); 583 | auto effectCtrl1 = this._buildKnob( 584 | typedParam!(Params.effectCtrl1)(_params), 585 | rectangle(effectKind.max.x + marginW, effectKind.min.y, knobRad, knobRad), 586 | "ctrl1"); 587 | auto effectCtrl2 = this._buildKnob( 588 | typedParam!(Params.effectCtrl2)(_params), 589 | rectangle(effectCtrl1.min.x, effectCtrl1.max.y + marginH, knobRad, knobRad), 590 | "ctrl2"); 591 | auto effectMix = this._buildKnob( 592 | typedParam!(Params.effectMix)(_params), 593 | rectangle(effectCtrl2.min.x, effectCtrl2.max.y + marginH, knobRad, knobRad), 594 | "mix"); 595 | return expand(effectLabel.position, effectKind, 596 | effectCtrl1, effectCtrl2, effectMix); 597 | } 598 | 599 | /// Builds the "EQ" section. 600 | box2i _buildEQ(int x, int y) { 601 | auto label = _addLabel("EQ/Pan", x, y, fontMedium); 602 | auto freq = _buildKnob( 603 | typedParam!(Params.eqFreq)(_params), 604 | rectangle(x, label.position.max.y + marginH, knobRad, knobRad), "freq"); 605 | auto level = _buildKnob( 606 | typedParam!(Params.eqLevel)(_params), 607 | rectangle(x, freq.max.y + marginH, knobRad, knobRad), "gain"); 608 | auto q = _buildKnob( 609 | typedParam!(Params.eqQ)(_params), 610 | rectangle(x, level.max.y + marginH, knobRad, knobRad), "Q"); 611 | auto tone = _buildKnob( 612 | typedParam!(Params.eqTone)(_params), 613 | rectangle(freq.max.x + marginW, freq.min.y, knobRad, knobRad), "tone"); 614 | auto pan = _buildKnob( 615 | typedParam!(Params.eqPan)(_params), 616 | rectangle(tone.min.x, tone.max.y + marginH, knobRad, knobRad), "L-R"); 617 | return expand(label.position, freq, level, q, tone, pan); 618 | } 619 | 620 | /// Build "LFO" section. 621 | box2i _buildLFO(Params offset)(string label, int x, int y) { 622 | auto lfo1Label = this._addLabel(label, x, y, fontMedium); 623 | auto lfo1Wave = this._buildSlider( 624 | _params[Params.lfo1Wave + offset], 625 | rectangle(lfo1Label.position.min.x, lfo1Label.position.max.y + marginH, 626 | slideWidth, slideHeight), "wave", waveNames); 627 | auto lfo1Amount = this._buildKnob( 628 | typedParam!(Params.lfo1Amount + offset)(_params), 629 | rectangle(lfo1Wave.max.x + marginW, lfo1Wave.min.y, knobRad, knobRad), 630 | "amt"); 631 | auto lfo1Speed = this._buildKnob( 632 | typedParam!(Params.lfo1Speed + offset)(_params), 633 | rectangle(lfo1Amount.min.x, lfo1Amount.max.y + marginH, knobRad, knobRad), 634 | "spd"); 635 | auto lfo1Sync = this._buildSwitch( 636 | typedParam!(Params.lfo1Sync + offset)(_params), 637 | rectangle(lfo1Speed.min.x, lfo1Speed.max.y + marginH, knobRad, knobRad), 638 | "sync"); 639 | auto lfo1Trigger = this._buildSwitch( 640 | typedParam!(Params.lfo1Trigger + offset)(_params), 641 | rectangle(lfo1Sync.max.x + marginW, lfo1Sync.min.y, knobRad, knobRad), 642 | "trig"); 643 | auto lfo1Mul= this._buildSlider( 644 | _params[Params.lfo1Mul + offset], 645 | rectangle(lfo1Amount.max.x, lfo1Amount.min.y, 646 | slideWidth, knobRad * 2 + fontSmall + marginH), 647 | "note", mulNames); 648 | auto lfo1Dest = this._buildSlider( 649 | _params[Params.lfo1Dest + offset], 650 | rectangle(lfo1Mul.max.x + marginW, lfo1Mul.min.y, slideWidth, slideHeight), 651 | "dst", lfoDestNames); 652 | return expand(lfo1Label.position, lfo1Wave, lfo1Amount, 653 | lfo1Speed, lfo1Sync, lfo1Trigger, lfo1Mul, lfo1Dest); 654 | } 655 | 656 | box2i _buildSlider(Parameter p, box2i pos, string label, const string[] vlabels) { 657 | UISlider ui = mallocNew!UISlider(this.context, p); 658 | pos.width(pos.width / 2); 659 | ui.position = pos; 660 | ui.trailWidth = 0.5; 661 | ui.handleWidthRatio = 0.5; 662 | ui.handleHeightRatio = cast(float) fontSmall / pos.height; 663 | ui.handleStyle = HandleStyle.shapeBlock; 664 | ui.handleMaterial = RGBA(0, 0, 0, 0); // smooth, metal, shiny, phisycal 665 | ui.handleDiffuse = handleDiffuse; // RGBA(255, 255, 255, 0); 666 | ui.litTrailDiffuse = litTrailDiffuse; 667 | ui.litTrailDiffuseAlt = litTrailDiffuse; 668 | ui.unlitTrailDiffuse = unlitTrailDiffuse; 669 | this.addChild(ui); 670 | 671 | box2i ret = ui.position; 672 | if (vlabels.length > 0) { 673 | const labelHeight = cast(double) pos.height / vlabels.length; 674 | int maxlen = 0; 675 | foreach (lab; vlabels) { 676 | maxlen = max(maxlen, cast(int) lab.length); 677 | } 678 | foreach (i, lab; vlabels) { 679 | const y = cast(uint) (pos.min.y + (vlabels.length - i - 1) * labelHeight); 680 | UILabel l = _addLabel(lab, pos.max.x, y, fontSmall); 681 | l.width(maxlen * fontSmallW); 682 | ret = ret.expand(l.position); 683 | } 684 | } 685 | 686 | if (label == "") { 687 | return ret; 688 | } 689 | auto lab = this._addLabel(label, pos.min.x, pos.max.y + marginH, fontSmall); 690 | lab.width = ret.width; 691 | return ret.expand(lab.position); 692 | } 693 | 694 | box2i _buildKnob(FloatParameter p, box2i pos, string label) { 695 | UIKnob knob = mallocNew!UIKnob(this.context, p); 696 | this.addChild(knob); 697 | knob.position(pos); 698 | knob.knobDiffuse = knobDiffuse; // RGBA(70, 70, 70, 0); 699 | knob.style = KnobStyle.ball; 700 | knob.knobMaterial = RGBA(255, 0, 0, 0); // smooth, metal, shiny, phisycal 701 | knob.LEDDepth = 0; 702 | knob.numLEDs = 0; 703 | knob.knobRadius = 0.5; 704 | knob.trailRadiusMin = 0.4; 705 | knob.trailRadiusMax = 1; 706 | knob.LEDDiffuseLit = RGBA(0, 0, 0, 0); 707 | knob.LEDDiffuseUnlit = RGBA(0, 0, 0, 0); 708 | knob.LEDRadiusMin = 0f; 709 | knob.LEDRadiusMax = 0f; 710 | 711 | knob.litTrailDiffuse = handleDiffuse; // litTrailDiffuse; 712 | knob.unlitTrailDiffuse = unlitTrailDiffuse; 713 | auto lab = this._addLabel(label, knob.position.min.x, knob.position.max.y, fontSmall); 714 | // TODO: margin. 715 | lab.width = knob.position.width; 716 | return expand(knob.position, lab.position); 717 | } 718 | 719 | box2i _buildSwitch(BoolParameter p, box2i pos, string label) { 720 | UIOnOffSwitch ui = mallocNew!UIOnOffSwitch(this.context, p); 721 | ui.position = pos; 722 | ui.diffuseOn = handleDiffuse; 723 | ui.diffuseOff = litTrailDiffuse; 724 | this.addChild(ui); 725 | auto lab = this._addLabel(label, pos.min.x, pos.max.y, fontSmall); 726 | lab.width = ui.width; 727 | return expand(ui.position, lab.position); 728 | } 729 | 730 | UILabel _addLabel(string text, int x, int y, int fontSize) { 731 | UILabel label; 732 | this.addChild(label = mallocNew!UILabel(context(), _font, text)); 733 | label.textColor(fontColor); 734 | label.textSize(fontSize); 735 | label.position(rectangle(x, y, cast(int) (fontSize * text.length * 0.8), 736 | fontSize)); 737 | return label; 738 | } 739 | 740 | 741 | enum marginW = 5; 742 | enum marginH = 5; 743 | enum screenWidth = 640; 744 | enum screenHeight = 480; 745 | 746 | enum fontLarge = 16; 747 | enum fontMedium = 12; 748 | enum fontMediumW = cast(int) (fontMedium * 0.8); 749 | enum fontSmall = 9; 750 | enum fontSmallW = cast(int) (fontSmall * 0.8); 751 | 752 | enum knobRad = 25; 753 | enum slideWidth = 40; 754 | enum slideHeight = 100; 755 | 756 | enum litTrailDiffuse = RGBA(99, 61, 24, 20); 757 | enum handleDiffuse = RGBA(240, 127, 17, 40); 758 | enum unlitTrailDiffuse = RGBA(29, 29, 29, 20); 759 | enum fontColor = RGBA(253, 250, 243, 0); 760 | enum knobDiffuse = RGBA(65, 65, 65, 0); // RGBA(216, 216, 216, 0); 761 | enum litSwitchOn = 40; 762 | 763 | static immutable pitchLabels = ["-12", "-6", "0", "6", "12"]; 764 | static immutable waveNames = ["sin", "saw", "pls", "tri", "rnd"]; 765 | 766 | Font _font; 767 | UILabel _tempo, _synth2, _date; 768 | char[10] _tempoStr; 769 | double _tempoValue; 770 | UILabel _poly, _chorusMulti; 771 | char[3] _polyStr, _chorusMultiStr; 772 | Parameter[] _params; 773 | UIWindowResizer _resizerHint; 774 | Vec!box2i _defaultRects; 775 | Vec!float _defaultTextSize; 776 | } 777 | -------------------------------------------------------------------------------- /source/synth2/lfo.d: -------------------------------------------------------------------------------- 1 | /** 2 | Synth2 LFO (low freq osc) module. 3 | 4 | Copyright: klknn 2021. 5 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module synth2.lfo; 8 | 9 | import std.algorithm.comparison : min; 10 | import std.traits : EnumMembers; 11 | 12 | import dplug.client.client : TimeInfo; 13 | import dplug.client.midi : MidiMessage; 14 | 15 | import synth2.waveform : Waveform, WaveformRange; 16 | 17 | /// Note duration relative to bars. 18 | enum Bar { 19 | x32 = 32f, 20 | x16 = 16f, 21 | x8 = 8f, 22 | x4 = 4f, 23 | x2 = 2f, 24 | x1 = 1f, 25 | x1_2 = 1f / 2f, 26 | x1_4 = 1f / 4f, 27 | x1_8 = 1f / 8f, 28 | x1_16 = 1f / 16f, 29 | x1_32 = 1f / 32f, 30 | } 31 | 32 | /// Bar multiplier. 33 | enum Multiplier { 34 | dot, 35 | none, 36 | tri, 37 | } 38 | 39 | /// String names of Multiplier. 40 | static immutable multiplierNames = [__traits(allMembers, Multiplier)]; 41 | 42 | /// Multiplier conversions to float. 43 | static immutable float[multiplierNames.length] mulToFloat = [ 44 | Multiplier.dot: 1.5f, Multiplier.none: 1f, Multiplier.tri: 1f / 3 ]; 45 | 46 | /// Inteval for notes. 47 | struct Interval { 48 | /// 49 | Bar bar; 50 | /// 51 | Multiplier mul; 52 | 53 | @nogc nothrow pure @safe: 54 | 55 | /// Returns: float interval value in bar. 56 | float toFloat() const { 57 | return bar * mulToFloat[mul]; 58 | } 59 | 60 | alias toFloat this; 61 | } 62 | 63 | @nogc nothrow pure @safe 64 | unittest { 65 | import std.math : isClose; 66 | 67 | assert(Interval(Bar.x1_8, Multiplier.none).toFloat == 1f / 8); 68 | assert(Interval(Bar.x1_8, Multiplier.dot).toFloat == 1f / 8 * 1.5); 69 | assert(isClose(Interval(Bar.x1_8, Multiplier.tri).toFloat, 1f / 8 / 3)); 70 | } 71 | 72 | /// Converts a float value into an Interval object. 73 | /// Params: 74 | /// x = float value. 75 | /// Returns: Interval. 76 | @nogc nothrow pure @safe 77 | Bar toBar(float x) { 78 | assert(0 <= x && x <= 1); 79 | static immutable bars = [EnumMembers!Bar]; 80 | return bars[cast(int) (x * ($ - 1))]; 81 | } 82 | 83 | @nogc nothrow pure @safe 84 | unittest { 85 | assert(1.toBar == Bar.x1_32); 86 | assert(0.5.toBar == Bar.x1); 87 | assert(0.toBar == Bar.x32); 88 | } 89 | 90 | /// Converts interval object with tempo to seconds. 91 | /// Params: 92 | /// i = interval object. 93 | /// tempo = host tempo. 94 | /// Returns: seconds. 95 | float toSeconds(Interval i, float tempo) @nogc nothrow pure @safe { 96 | // bars / 4 (beat sec) * 60 (beat min) / bpm 97 | return i.toFloat / 4 * 60 / tempo; 98 | } 99 | 100 | /// Low freq osc. 101 | struct LFO { 102 | @nogc nothrow @safe: 103 | 104 | void setSampleRate(float sampleRate) pure { 105 | _wave.sampleRate = sampleRate; 106 | _nplay = 0; 107 | } 108 | 109 | /// Sets LFO parameters. 110 | /// Params: 111 | /// waveform = waveform type. 112 | /// sync = flag to sync tempo. 113 | /// normalizedSpeed = [0, 1] value to control speed. 114 | /// mult = duration multiplier for sync. 115 | /// tinfo = info on bpm etc. 116 | void setParams(Waveform waveform, bool sync, float normalizedSpeed, 117 | Multiplier mult, TimeInfo tinfo) pure { 118 | // TODO: create separated functions for sync and non-sync. 119 | _wave.waveform = waveform; 120 | if (sync) { 121 | _wave.freq = 1f / Interval(normalizedSpeed.toBar, mult).toSeconds(tinfo.tempo); 122 | } 123 | else { 124 | _wave.freq = normalizedSpeed * 10; 125 | } 126 | // FIXME: this makes sound glitch. 127 | // if (tinfo.hostIsPlaying) { 128 | // _wave.popFront(tinfo.timeInSamples); 129 | // } 130 | } 131 | 132 | void setMidi(MidiMessage midi) pure @system { 133 | if (midi.isNoteOn) { 134 | if (_nplay == 0) { 135 | _wave.phase = 0; 136 | } 137 | ++_nplay; 138 | } 139 | else if (midi.isNoteOff) { 140 | --_nplay; 141 | } 142 | } 143 | 144 | /// Returns: the current LFO amplitude. 145 | float front() const { return _wave.front; } 146 | 147 | /// Increments LFO timestamp. 148 | void popFront() pure { _wave.popFront(); } 149 | 150 | alias empty = _wave.empty; 151 | 152 | private: 153 | int _nplay; 154 | WaveformRange _wave; 155 | } 156 | -------------------------------------------------------------------------------- /source/synth2/modfilter.d: -------------------------------------------------------------------------------- 1 | /** 2 | Synth2 modulated filters. 3 | 4 | Copyright: klknn 2021. 5 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module synth2.modfilter; 8 | 9 | import dplug.client.midi; 10 | import mir.math.common : fmin, fmax, fastmath; 11 | 12 | import synth2.envelope; 13 | import synth2.filter; 14 | 15 | /// Filter with MIDI and ADSR modulation. 16 | struct ModFilter { 17 | /// Filter to be modurated. 18 | Filter filter; 19 | 20 | /// Modulating envelope. 21 | ADSR envelope; 22 | 23 | alias filter this; 24 | 25 | /// Use MIDI velocity for scaling the envelope. 26 | bool useVelocity = false; 27 | 28 | /// Constant scaling factor for the envelope. 29 | float envAmount = 0; 30 | 31 | /// Constant scaling factor for MIDI frequency. 32 | float trackAmount = 0; 33 | 34 | @nogc nothrow pure @safe @fastmath: 35 | 36 | void setParams(FilterKind kind, float freqPercent, float q) { 37 | this.cutoff = freqPercent; 38 | this.q = q; 39 | this.kind = kind; 40 | this.filter.setParams(this.kind, this.cutoff, this.q); 41 | } 42 | 43 | void setSampleRate(float sampleRate) { 44 | this.filter.setSampleRate(sampleRate); 45 | this.envelope.setSampleRate(sampleRate); 46 | this.cutoffDiff = 0; 47 | } 48 | 49 | void setCutoffDiff(float diff) { 50 | this.cutoffDiff = diff; 51 | } 52 | 53 | /// Increments envlope timestamp. 54 | void popFront() { 55 | const cutoff = fmax(0f, fmin(1f, this.cutoff + this.cutoffDiff + this.track + 56 | this.velocity * this.envelope.front)); 57 | this.filter.setParams(this.kind, cutoff, this.q); 58 | this.envelope.popFront(); 59 | } 60 | 61 | @system void setMidi(MidiMessage msg) { 62 | if (msg.isNoteOn) { 63 | this.velocity = this.envAmount * 64 | (this.useVelocity ? msg.noteVelocity / 127 : 1); 65 | this.track = this.trackAmount * msg.noteNumber / 127; 66 | } 67 | this.envelope.setMidi(msg); 68 | } 69 | 70 | private: 71 | // filter states 72 | float cutoff = 0; 73 | float cutoffDiff = 0; 74 | float q = 0; 75 | FilterKind kind; 76 | float velocity = 1; 77 | float track = 0; 78 | } 79 | -------------------------------------------------------------------------------- /source/synth2/oscillator.d: -------------------------------------------------------------------------------- 1 | /** 2 | Ocillator module. 3 | 4 | Copyright: klknn 2021. 5 | Copyright: Elias Batek 2018. 6 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 7 | */ 8 | module synth2.oscillator; 9 | 10 | import dplug.client.midi : MidiMessage, MidiStatus; 11 | import mir.math : log2, exp2, fastmath, PI; 12 | 13 | import synth2.waveform : Waveform, WaveformRange; 14 | import synth2.voice : VoiceStatus; 15 | 16 | @safe nothrow @nogc: 17 | 18 | /// Converts MIDI velocity to gain. 19 | /// Params: 20 | /// velocity = MIDI velocity [0, 127]. 21 | /// sensitivity = gain sensitivity. 22 | /// bias = gain bias. 23 | /// Returns: Maps 0 to 127 into [0, 1] level with affine transformation. 24 | float velocityToLevel( 25 | float velocity, float sensitivity = 1.0, float bias = 0.1) @fastmath pure { 26 | assert(0 <= velocity && velocity <= 127); 27 | return (velocity / 127f - bias) * sensitivity + bias; 28 | } 29 | 30 | /// Converts MIDI note to frequency. 31 | /// Params: 32 | /// note = MIDI note number [0, 127]. 33 | /// Returns: frequency [Hz]. 34 | float convertMIDINoteToFrequency(float note) @fastmath pure { 35 | return 440.0f * exp2((note - 69.0f) / 12.0f); 36 | } 37 | 38 | /// Polyphonic oscillator that generates WAV samples by given params and midi. 39 | struct Oscillator 40 | { 41 | public: 42 | @safe @nogc nothrow @fastmath: 43 | 44 | // Setters 45 | void setInitialPhase(float value) pure { 46 | _initialPhase = value; 47 | } 48 | 49 | void setWaveform(Waveform value) pure { 50 | foreach (ref w; _waves) { 51 | w.waveform = value; 52 | } 53 | } 54 | 55 | void setPulseWidth(float value) pure { 56 | foreach (ref w; _waves) { 57 | w.pulseWidth = value; 58 | } 59 | } 60 | 61 | void setSampleRate(float sampleRate) pure { 62 | foreach (ref v; _voicesArr) { 63 | v.setSampleRate(sampleRate); 64 | } 65 | foreach (ref w; _wavesArr) { 66 | w.sampleRate = sampleRate; 67 | w.phase = 0; 68 | } 69 | } 70 | 71 | void setVelocitySense(float value) pure { 72 | _velocitySense = value; 73 | } 74 | 75 | void setMidi(MidiMessage msg) @system { 76 | if (msg.isNoteOn) { 77 | markNoteOn(msg); 78 | } 79 | if (msg.isNoteOff) { 80 | markNoteOff(msg.noteNumber()); 81 | } 82 | if (msg.isPitchBend) { 83 | _pitchBend = msg.pitchBend(); 84 | } 85 | } 86 | 87 | void setNoteTrack(bool b) pure { 88 | _noteTrack = b; 89 | } 90 | 91 | void setNoteDiff(float note) pure { 92 | _noteDiff = note; 93 | } 94 | 95 | void setNoteDetune(float val) pure { 96 | _noteDetune = val; 97 | } 98 | 99 | /// Syncronize osc phase to the given src osc phase. 100 | /// Params: 101 | /// src = modulating osc. 102 | void synchronize(const ref Oscillator src) pure { 103 | foreach (i, ref w; _waves) { 104 | if (src._waves[i].normalized) { 105 | w.phase = 0f; 106 | } 107 | } 108 | } 109 | 110 | void setFM(float scale, const ref Oscillator mod) { 111 | foreach (i, ref w; _waves) { 112 | w.phase += scale * mod._voices[i].front; 113 | } 114 | } 115 | 116 | void setADSR(float a, float d, float s, float r) pure { 117 | foreach (ref v; _voices) { 118 | v.setADSR(a, d, s, r); 119 | } 120 | } 121 | 122 | /// Infinite range method. 123 | enum empty = false; 124 | 125 | /// Returns: sum of amplitudes of _waves at the current phase. 126 | float front() const pure { 127 | float sample = 0; 128 | foreach (i, ref v; _voices) { 129 | sample += v.front * _waves[i].front; 130 | } 131 | return sample / _voicesArr.length; 132 | } 133 | 134 | /// Increments phase in _waves. 135 | void popFront() pure { 136 | foreach (ref v; _voices) { 137 | v.popFront(); 138 | } 139 | foreach (ref w; _waves) { 140 | w.popFront(); 141 | } 142 | } 143 | 144 | /// Updates frequency by MIDI and params. 145 | void updateFreq() pure @system { 146 | foreach (i, ref v; _voices) { 147 | if (v.isPlaying) { 148 | _waves[i].freq = convertMIDINoteToFrequency(_note(v)); 149 | } 150 | } 151 | } 152 | 153 | /// Returns: true if any voices are playing. 154 | bool isPlaying() const pure { 155 | foreach (ref v; _voices) { 156 | if (v.isPlaying) return true; 157 | } 158 | return false; 159 | } 160 | 161 | /// Returns: the waveform object used last. 162 | WaveformRange lastUsedWave() const pure { 163 | return _waves[_lastUsedId]; 164 | } 165 | 166 | void setVoice(int n, bool legato, float portament, bool autoPortament) { 167 | assert(n <= _voicesArr.length, "Exceeds allocated voices."); 168 | assert(0 <= n, "MaxVoices must be positive."); 169 | _maxVoices = n; 170 | foreach (ref v; _voices) { 171 | v.setParams(legato, portament, autoPortament); 172 | } 173 | } 174 | 175 | private: 176 | float _note(const ref VoiceStatus v) const pure { 177 | return (_noteTrack ? v.note : 69.0f) + _noteDiff + _noteDetune 178 | + _pitchBend * _pitchBendWidth; 179 | } 180 | 181 | size_t _newVoiceId() const pure { 182 | foreach (i, ref v; _voices) { 183 | if (!v.isPlaying) { 184 | return i; 185 | } 186 | } 187 | return (_lastUsedId + 1) % _voices.length; 188 | } 189 | 190 | void markNoteOn(MidiMessage midi) pure @system { 191 | const i = _newVoiceId; 192 | const level = velocityToLevel(midi.noteVelocity(), _velocitySense); 193 | _voices[i].play(midi.noteNumber(), level); 194 | if (_initialPhase != -PI) 195 | _waves[i].phase = _initialPhase; 196 | _lastUsedId = i; 197 | } 198 | 199 | void markNoteOff(int note) pure { 200 | foreach (ref v; _voices) { 201 | v.stop(note); 202 | } 203 | } 204 | 205 | inout(VoiceStatus)[] _voices() inout pure return { 206 | return _voicesArr[0 .. _maxVoices]; 207 | } 208 | 209 | inout(WaveformRange)[] _waves() inout pure return { 210 | return _wavesArr[0 .. _maxVoices]; 211 | } 212 | 213 | // voice global config 214 | float _initialPhase = 0.0; 215 | float _noteDiff = 0.0; 216 | float _noteDetune = 0.0; 217 | bool _noteTrack = true; 218 | float _velocitySense = 0.0; 219 | float _pitchBend = 0.0; 220 | float _pitchBendWidth = 2.0; 221 | size_t _lastUsedId = 0; 222 | size_t _maxVoices = _voicesArr.length; 223 | 224 | enum numVoices = 16; 225 | VoiceStatus[numVoices] _voicesArr; 226 | WaveformRange[numVoices] _wavesArr; 227 | } 228 | -------------------------------------------------------------------------------- /source/synth2/params.d: -------------------------------------------------------------------------------- 1 | /** 2 | Synth2 parameters. 3 | 4 | Copyright: klknn 2021. 5 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module synth2.params; 8 | 9 | import std.traits : EnumMembers; 10 | 11 | import dplug.core.nogc : destroyFree, mallocNew; 12 | import dplug.core.vec : makeVec, Vec; 13 | import dplug.client.params : BoolParameter, EnumParameter, GainParameter, 14 | IntegerParameter, LinearFloatParameter, LogFloatParameter, Parameter; 15 | import mir.math.constant : PI; 16 | 17 | import synth2.delay : DelayKind, delayNames; 18 | import synth2.effect : EffectKind, effectNames; 19 | import synth2.waveform : Waveform, waveformNames; 20 | import synth2.filter : filterNames, FilterKind; 21 | import synth2.lfo : Multiplier, multiplierNames; 22 | 23 | /// Parameter ids. 24 | enum Params : int { 25 | /// Oscillator section 26 | osc1Waveform, 27 | osc1Det, 28 | osc1FM, 29 | 30 | osc2Waveform, 31 | osc2Ring, 32 | osc2Sync, 33 | osc2Track, 34 | osc2Pitch, 35 | osc2Fine, 36 | 37 | oscMix, 38 | oscKeyShift, 39 | oscTune, 40 | oscPhase, 41 | 42 | oscPulseWidth, 43 | oscSubWaveform, 44 | oscSubVol, 45 | oscSubOct, 46 | 47 | /// Amp section 48 | ampAttack, 49 | ampDecay, 50 | ampSustain, 51 | ampRelease, 52 | ampGain, 53 | ampVel, 54 | 55 | /// Filter section 56 | filterKind, 57 | filterCutoff, 58 | filterQ, 59 | filterTrack, 60 | filterAttack, 61 | filterDecay, 62 | filterSustain, 63 | filterRelease, 64 | filterEnvAmount, 65 | filterUseVelocity, 66 | saturation, 67 | 68 | /// Mod envelope 69 | menvDest, 70 | menvAttack, 71 | menvDecay, 72 | menvAmount, 73 | 74 | /// LFO1 75 | lfo1Wave, 76 | lfo1Dest, 77 | lfo1Sync, 78 | lfo1Speed, 79 | lfo1Mul, 80 | lfo1Amount, 81 | lfo1Trigger, 82 | 83 | /// LFO2 84 | lfo2Wave, 85 | lfo2Dest, 86 | lfo2Sync, 87 | lfo2Speed, 88 | lfo2Mul, 89 | lfo2Amount, 90 | lfo2Trigger, 91 | 92 | /// Effect 93 | // TODO: add effectOn param. 94 | effectKind, 95 | effectCtrl1, 96 | effectCtrl2, 97 | effectMix, 98 | 99 | // Equalizer / Pan 100 | eqFreq, 101 | eqLevel, 102 | eqQ, 103 | eqTone, 104 | eqPan, 105 | 106 | // Voice 107 | voiceKind, 108 | voicePoly, 109 | voicePortament, 110 | voicePortamentAuto, 111 | 112 | // Delay 113 | // TODO: add delayOn param. 114 | delayKind, 115 | delayTime, 116 | delayMul, 117 | delaySpread, 118 | delayFeedback, 119 | delayTone, 120 | delayMix, 121 | 122 | // Chorus/flanger 123 | chorusOn, 124 | chorusMulti, 125 | chorusTime, 126 | chorusDepth, 127 | chorusRate, 128 | chorusFeedback, 129 | chorusLevel, 130 | chorusWidth, 131 | } 132 | 133 | static immutable paramNames = [__traits(allMembers, Params)]; 134 | 135 | /// Modulation envelope destination. 136 | enum MEnvDest { 137 | osc2, 138 | fm, 139 | pw, 140 | } 141 | 142 | static immutable menvDestNames = [__traits(allMembers, MEnvDest)]; 143 | 144 | /// LFO modulation destination. 145 | enum LfoDest { 146 | osc2, 147 | osc12, 148 | filter, 149 | amp, 150 | pw, 151 | fm, 152 | pan, 153 | } 154 | 155 | static immutable lfoDestNames = [__traits(allMembers, LfoDest)]; 156 | 157 | /// Voice kind. 158 | enum VoiceKind { 159 | poly, 160 | mono, 161 | legato, 162 | } 163 | 164 | enum maxPoly = 16; 165 | static immutable voiceKindNames = [__traits(allMembers, VoiceKind)]; 166 | 167 | /// Setup default parameter. 168 | struct ParamBuilder { 169 | 170 | static osc1Waveform() { 171 | return mallocNew!EnumParameter( 172 | Params.osc1Waveform, "Osc1/Wave", waveformNames, Waveform.sine); 173 | } 174 | 175 | static osc1Det() { 176 | return mallocNew!LinearFloatParameter( 177 | Params.osc1Det, "Osc1/Detune", "", 0, 1, 0); 178 | } 179 | 180 | static osc1FM() { 181 | return mallocNew!LinearFloatParameter( 182 | Params.osc1FM, "Osc1/FM", "", 0, 10, 0); 183 | } 184 | 185 | static osc2Waveform() { 186 | return mallocNew!EnumParameter( 187 | Params.osc2Waveform, "Osc2/Wave", waveformNames, Waveform.triangle); 188 | } 189 | 190 | static osc2Track() { 191 | return mallocNew!BoolParameter(Params.osc2Track, "Osc2/Track", true); 192 | } 193 | 194 | // TODO: check synth1 default (440hz?) 195 | static osc2Pitch() { 196 | return mallocNew!IntegerParameter( 197 | Params.osc2Pitch, "Osc2/Pitch", "", -12, 12, 0); 198 | } 199 | 200 | static osc2Fine() { 201 | return mallocNew!LinearFloatParameter( 202 | Params.osc2Fine, "Osc2/Fine", "", -1.0, 1.0, 0.0); 203 | } 204 | 205 | static osc2Ring() { 206 | return mallocNew!BoolParameter(Params.osc2Ring, "Osc2/Ring", false); 207 | } 208 | 209 | static osc2Sync() { 210 | return mallocNew!BoolParameter(Params.osc2Sync, "Osc2/Sync", false); 211 | } 212 | 213 | static oscMix() { 214 | return mallocNew!LinearFloatParameter( 215 | Params.oscMix, "Osc/Mix", "", 0f, 1f, 0f); 216 | } 217 | 218 | static oscKeyShift() { 219 | return mallocNew!IntegerParameter( 220 | Params.oscKeyShift, "Osc/KeyShift", "semitone", -12, 12, 0); 221 | } 222 | 223 | static oscTune() { 224 | return mallocNew!LinearFloatParameter( 225 | Params.oscTune, "Osc/Tune", "cent", -1.0, 1.0, 0); 226 | } 227 | 228 | enum float ignoreOscPhase = -PI; 229 | 230 | static oscPhase() { 231 | return mallocNew!LinearFloatParameter( 232 | Params.oscPhase, "Osc/Phase", "", -PI, PI, ignoreOscPhase); 233 | } 234 | 235 | static oscPulseWidth() { 236 | return mallocNew!LinearFloatParameter( 237 | Params.oscPulseWidth, "Osc/PW", "", 0f, 1f, 0.5f); 238 | } 239 | 240 | static oscSubWaveform() { 241 | return mallocNew!EnumParameter( 242 | Params.oscSubWaveform, "OscSub/Wave", waveformNames, Waveform.sine); 243 | } 244 | 245 | // TODO: check synth1 max vol. 246 | static oscSubVol() { 247 | return mallocNew!GainParameter( 248 | Params.oscSubVol, "OscSub/Gain", 3, -float.infinity); 249 | } 250 | 251 | static oscSubOct() { 252 | return mallocNew!BoolParameter(Params.oscSubOct, "OscSub/-1Oct", false); 253 | } 254 | 255 | // Epsilon value to avoid NaN in log. 256 | enum logBias = 1e-3; 257 | 258 | static ampAttack() { 259 | return mallocNew!LogFloatParameter( 260 | Params.ampAttack, "Amp/Attack", "sec", logBias, 100.0, logBias); 261 | } 262 | 263 | static ampDecay() { 264 | return mallocNew!LogFloatParameter( 265 | Params.ampDecay, "Amp/Decay", "sec", logBias, 100.0, logBias); 266 | } 267 | 268 | static ampSustain() { 269 | return mallocNew!GainParameter( 270 | Params.ampSustain, "Amp/Sustain", 0.0, 0.0); 271 | } 272 | 273 | static ampRelease() { 274 | return mallocNew!LogFloatParameter( 275 | Params.ampRelease, "Amp/Release", "sec", logBias, 100, logBias); 276 | } 277 | 278 | static ampGain() { 279 | return mallocNew!GainParameter(Params.ampGain, "Amp/Gain", 3.0, 0.0); 280 | } 281 | 282 | static ampVel() { 283 | return mallocNew!LinearFloatParameter( 284 | Params.ampVel, "Amp/Velocity", "", 0, 1.0, 1.0); 285 | } 286 | 287 | static filterKind() { 288 | return mallocNew!EnumParameter( 289 | Params.filterKind, "Filter/Kind", filterNames, FilterKind.LP12); 290 | } 291 | 292 | static filterCutoff() { 293 | return mallocNew!LogFloatParameter( 294 | Params.filterCutoff, "Filter/Cutoff", "", logBias, 1, 1); 295 | } 296 | 297 | static filterQ() { 298 | return mallocNew!LinearFloatParameter( 299 | Params.filterQ, "Filter/Q", "", 0, 1, 0); 300 | } 301 | 302 | static filterTrack() { 303 | return mallocNew!LinearFloatParameter( 304 | Params.filterTrack, "Filter/Track", "", 0, 1, 0); 305 | } 306 | 307 | static filterEnvAmount() { 308 | return mallocNew!LinearFloatParameter( 309 | Params.filterEnvAmount, "Filter/Amount", "", 0, 1, 0); 310 | } 311 | 312 | static filterAttack() { 313 | return mallocNew!LogFloatParameter( 314 | Params.filterAttack, "Filter/Attack", "sec", logBias, 100.0, logBias); 315 | } 316 | 317 | static filterDecay() { 318 | return mallocNew!LogFloatParameter( 319 | Params.filterDecay, "Filter/Decay", "sec", logBias, 100.0, logBias); 320 | } 321 | 322 | static filterSustain() { 323 | return mallocNew!GainParameter( 324 | Params.filterSustain, "Filter/Sustain", 0.0, 0.0); 325 | } 326 | 327 | static filterRelease() { 328 | return mallocNew!LogFloatParameter( 329 | Params.filterRelease, "Filter/Release", "sec", logBias, 100, logBias); 330 | } 331 | 332 | static filterUseVelocity() { 333 | return mallocNew!BoolParameter( 334 | Params.filterUseVelocity, "Filter/Velocity", false); 335 | } 336 | 337 | static saturation() { 338 | return mallocNew!LinearFloatParameter( 339 | Params.saturation, "Saturation", "", 0, 100, 0); 340 | } 341 | 342 | static menvDest() { 343 | return mallocNew!EnumParameter( 344 | Params.menvDest, "MEnv/Destination", menvDestNames, MEnvDest.osc2); 345 | } 346 | 347 | static menvAttack() { 348 | return mallocNew!LogFloatParameter( 349 | Params.menvAttack, "MEnv/Attack", "sec", logBias, 100.0, logBias); 350 | } 351 | 352 | static menvDecay() { 353 | return mallocNew!LogFloatParameter( 354 | Params.menvDecay, "MEnv/Decay", "sec", logBias, 100.0, logBias); 355 | } 356 | 357 | static menvAmount() { 358 | return mallocNew!LinearFloatParameter( 359 | Params.menvAmount, "MEnv/Amount", "", -100, 100, 0); 360 | } 361 | 362 | static effectKind() { 363 | return mallocNew!EnumParameter( 364 | Params.effectKind, "Effect/Kind", effectNames, EffectKind.ad1); 365 | } 366 | 367 | static effectCtrl1() { 368 | return mallocNew!LinearFloatParameter( 369 | Params.effectCtrl1, "Effect/Ctrl1", "", 0, 1, 0.5); 370 | } 371 | 372 | static effectCtrl2() { 373 | return mallocNew!LinearFloatParameter( 374 | Params.effectCtrl2, "Effect/Ctrl2", "", 0, 1, 0.5); 375 | } 376 | 377 | static effectMix() { 378 | return mallocNew!LinearFloatParameter( 379 | Params.effectMix, "Effect/Mix", "", 0, 1, 0); 380 | } 381 | 382 | static lfo1Wave() { 383 | return mallocNew!EnumParameter( 384 | Params.lfo1Wave, "LFO1/Wave", waveformNames, Waveform.triangle); 385 | } 386 | 387 | static lfo1Dest() { 388 | return mallocNew!EnumParameter( 389 | Params.lfo1Dest, "LFO1/Dest", lfoDestNames, LfoDest.osc12); 390 | } 391 | 392 | static lfo1Sync() { 393 | return mallocNew!BoolParameter(Params.lfo1Sync, "LFO1/Sync", true); 394 | } 395 | 396 | static lfo1Speed() { 397 | return mallocNew!LinearFloatParameter( 398 | Params.lfo1Speed, "LFO1/Speed", "", 0, 1, 0.5); 399 | } 400 | 401 | static lfo1Mul() { 402 | return mallocNew!EnumParameter( 403 | Params.lfo1Mul, "LFO1/Mul", multiplierNames, Multiplier.none); 404 | } 405 | 406 | static lfo1Amount() { 407 | return mallocNew!LinearFloatParameter( 408 | Params.lfo1Amount, "LFO1/Amount", "", 0, 1, 0); 409 | } 410 | 411 | static lfo1Trigger() { 412 | return mallocNew!BoolParameter(Params.lfo1Trigger, "LFO1/trigger", false); 413 | } 414 | 415 | static lfo2Wave() { 416 | return mallocNew!EnumParameter( 417 | Params.lfo2Wave, "LFO2/Wave", waveformNames, Waveform.triangle); 418 | } 419 | 420 | static lfo2Dest() { 421 | return mallocNew!EnumParameter( 422 | Params.lfo2Dest, "LFO2/Dest", lfoDestNames, LfoDest.osc12); 423 | } 424 | 425 | static lfo2Sync() { 426 | return mallocNew!BoolParameter(Params.lfo2Sync, "LFO2/Sync", true); 427 | } 428 | 429 | static lfo2Speed() { 430 | return mallocNew!LinearFloatParameter( 431 | Params.lfo2Speed, "LFO2/Speed", "", 0, 1, 0.5); 432 | } 433 | 434 | static lfo2Mul() { 435 | return mallocNew!EnumParameter( 436 | Params.lfo2Mul, "LFO2/Mul", multiplierNames, Multiplier.none); 437 | } 438 | 439 | static lfo2Amount() { 440 | return mallocNew!LinearFloatParameter( 441 | Params.lfo2Amount, "LFO2/Amount", "", 0, 1, 0); 442 | } 443 | 444 | static lfo2Trigger() { 445 | return mallocNew!BoolParameter(Params.lfo2Trigger, "LFO2/Trigger", false); 446 | } 447 | 448 | static eqFreq() { 449 | return mallocNew!LogFloatParameter(Params.eqFreq, "EQ/Freq", "", logBias, 1, 0.5); 450 | } 451 | 452 | static eqLevel() { 453 | return mallocNew!LinearFloatParameter(Params.eqLevel, "EQ/Level", "", -1, 1, 0); 454 | } 455 | 456 | static eqQ() { 457 | return mallocNew!LinearFloatParameter(Params.eqQ, "EQ/Q", "", 0, 1, 0); 458 | } 459 | 460 | static eqTone() { 461 | return mallocNew!LinearFloatParameter(Params.eqTone, "EQ/Tone", "", -1, 1, 0); 462 | } 463 | 464 | static eqPan() { 465 | return mallocNew!LinearFloatParameter(Params.eqPan, "EQ/Pan", "", -1, 1, 0); 466 | } 467 | 468 | static voiceKind() { 469 | return mallocNew!EnumParameter( 470 | Params.voiceKind, "Voice/Kind", voiceKindNames, VoiceKind.poly); 471 | } 472 | 473 | static voicePoly() { 474 | return mallocNew!IntegerParameter( 475 | Params.voicePoly, "Voice/Poly", "voices", 0, maxPoly, maxPoly); 476 | } 477 | 478 | static voicePortament() { 479 | return mallocNew!LogFloatParameter( 480 | Params.voicePortament, "Voice/Port", "sec", logBias, 1, logBias); 481 | } 482 | 483 | static voicePortamentAuto() { 484 | return mallocNew!BoolParameter(Params.voicePortamentAuto, "Voice/Auto", true); 485 | } 486 | 487 | static delayKind() { 488 | return mallocNew!EnumParameter( 489 | Params.delayKind, "DelayKind", delayNames, DelayKind.st); 490 | } 491 | 492 | static delayTime() { 493 | return mallocNew!LinearFloatParameter( 494 | Params.delayTime, "Delay/Time", "", 0, 1, 1); 495 | } 496 | 497 | static delayMul() { 498 | return mallocNew!EnumParameter( 499 | Params.delayMul, "Delay/Mul", multiplierNames, Multiplier.none); 500 | } 501 | 502 | static delaySpread() { 503 | return mallocNew!LinearFloatParameter( 504 | Params.delaySpread, "Delay/Spread", "sec", 0, 0.1, 0); 505 | } 506 | 507 | static delayFeedback() { 508 | return mallocNew!LinearFloatParameter( 509 | Params.delayFeedback, "Delay/Feedback", "", 0, 1, 0); 510 | } 511 | 512 | static delayTone() { 513 | return mallocNew!LinearFloatParameter( 514 | Params.delayTone, "Delay/Tone", "", -1, 1, 0); 515 | } 516 | 517 | static delayMix() { 518 | return mallocNew!LinearFloatParameter( 519 | Params.delayMix, "Delay/Mix", "", 0, 1, 0); 520 | } 521 | 522 | static chorusOn() { 523 | return mallocNew!BoolParameter(Params.chorusOn, "Chrous/On", false); 524 | } 525 | 526 | static chorusMulti() { 527 | return mallocNew!IntegerParameter( 528 | Params.chorusMulti, "Chorus/Multi", "mul", 1, 4, 1); 529 | } 530 | 531 | static chorusTime() { 532 | return mallocNew!LinearFloatParameter( 533 | Params.chorusTime, "Chorus/Time", "ms", 3, 40, 11.1); 534 | } 535 | 536 | static chorusDepth() { 537 | return mallocNew!LinearFloatParameter( 538 | Params.chorusDepth, "Chorus/Depth", "", 0, 0.5, 0.25); 539 | } 540 | 541 | static chorusRate() { 542 | return mallocNew!LogFloatParameter( 543 | Params.chorusRate, "Chorus/Rate", "hz", logBias, 20, 0.86); 544 | } 545 | 546 | static chorusFeedback() { 547 | return mallocNew!LinearFloatParameter( 548 | Params.chorusFeedback, "Chorus/Feedback", "", 0, 1, 0); 549 | } 550 | 551 | static chorusLevel() { 552 | return mallocNew!GainParameter( 553 | Params.chorusLevel, "Chorus/Level", 5, 0.28); 554 | } 555 | 556 | static chorusWidth() { 557 | return mallocNew!LinearFloatParameter( 558 | Params.chorusWidth, "Chorus/Width", "", 0, 1, 0); 559 | } 560 | 561 | @nogc nothrow: 562 | static Parameter[] buildParameters() { 563 | auto params = makeVec!Parameter(EnumMembers!Params.length); 564 | static foreach (i, pname; paramNames) { 565 | params[i] = __traits(getMember, ParamBuilder, pname)(); 566 | assert(i == params[i].index, pname ~ " has wrong index."); 567 | } 568 | return params.releaseData(); 569 | } 570 | } 571 | 572 | /// Casts types from untyped parameters using parameter id. 573 | /// Params: 574 | /// pid = Params enum id. 575 | /// params = type-erased parameter array. 576 | /// Returns: statically-known typed param. 577 | auto typedParam(Params pid)(Parameter[] params) { 578 | alias T = typeof(__traits(getMember, ParamBuilder, paramNames[pid])()); 579 | auto ret = cast(T) params[pid]; 580 | assert(ret !is null); 581 | return ret; 582 | } 583 | 584 | /// 585 | @nogc nothrow 586 | unittest { 587 | Parameter[1] ps; 588 | ps[0] = ParamBuilder.osc1Waveform(); 589 | assert(is(typeof(typedParam!(Params.osc1Waveform)(ps[])) == EnumParameter)); 590 | } 591 | -------------------------------------------------------------------------------- /source/synth2/random.d: -------------------------------------------------------------------------------- 1 | module synth2.random; 2 | 3 | @nogc nothrow @safe pure 4 | uint rotl(const uint x, int k) { 5 | return (x << k) | (x >> (32 - k)); 6 | } 7 | 8 | @nogc nothrow @safe pure 9 | ulong splitmix64(ulong x) { 10 | ulong z = (x += 0x9e3779b97f4a7c15); 11 | z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; 12 | z = (z ^ (z >> 27)) * 0x94d049bb133111eb; 13 | return z ^ (z >> 31); 14 | } 15 | 16 | // Based on https://prng.di.unimi.it/xoshiro128plus.c 17 | struct Xorshiro128Plus { 18 | @nogc nothrow pure @safe: 19 | 20 | this(ulong seed) { 21 | s[0] = cast(uint) seed; 22 | s[1] = seed >> 32; 23 | 24 | ulong sp = splitmix64(seed); 25 | s[2] = cast(uint) sp; 26 | s[3] = sp >> 32; 27 | 28 | assert(!(s[0] == 0 && s[1] == 0 && s[2] == 0 && s[3] == 0)); 29 | } 30 | 31 | uint front() const { 32 | return s[0] + s[3]; 33 | } 34 | 35 | void popFront() { 36 | const uint t = s[1] << 9; 37 | 38 | s[2] ^= s[0]; 39 | s[3] ^= s[1]; 40 | s[1] ^= s[2]; 41 | s[0] ^= s[3]; 42 | 43 | s[2] ^= t; 44 | 45 | s[3] = rotl(s[3], 11); 46 | } 47 | 48 | /* This is the jump function for the generator. It is equivalent 49 | to 2^64 calls to popFront(); it can be used to generate 2^64 50 | non-overlapping subsequences for parallel computations. */ 51 | void jump() { 52 | _jump!([0x8764000b, 0xf542d2d3, 0x6fa035c3, 0x77f2db5b]); 53 | } 54 | 55 | /* This is the long-jump function for the generator. It is equivalent to 56 | 2^96 calls to popFront(); it can be used to generate 2^32 starting points, 57 | from each of which jump() will generate 2^32 non-overlapping 58 | subsequences for parallel distributed computations. */ 59 | void longJump() { 60 | _jump!([0xb523952e, 0x0b6f099f, 0xccf5a0ef, 0x1c580662]); 61 | } 62 | 63 | private: 64 | 65 | void _jump(const uint[4] JUMP)() { 66 | uint[4] a; 67 | foreach (j; JUMP) { 68 | foreach (b; 0 .. 32) { 69 | if (j & 1u << b) { 70 | a[] ^= s[]; 71 | } 72 | popFront(); 73 | } 74 | } 75 | s[] = a[]; 76 | } 77 | 78 | uint[4] s = [0, 0, cast(uint) splitmix64(0), splitmix64(0) >> 32]; 79 | } 80 | 81 | @nogc nothrow pure @safe 82 | unittest { 83 | Xorshiro128Plus rng0, rng1, rng2; 84 | assert(rng0.s == rng1.s, "initial seeds should be equal."); 85 | 86 | uint x0 = rng0.front(); 87 | assert(x0 == rng1.front(), "front should be the same if seeds are equal."); 88 | 89 | rng0.popFront(); 90 | assert(rng0.front != x0, "front should be changed by popFront."); 91 | 92 | rng1.popFront(); 93 | assert(rng0.front == rng1.front(), "popFront() should be reproducible"); 94 | 95 | rng1.jump(); 96 | assert(rng0.front != rng1.front, "jump() should mutate front."); 97 | 98 | rng0.jump(); 99 | assert(rng0.front == rng1.front, 100 | "Both rng0 and rng1 call 1 jump + 1 popFront in total."); 101 | 102 | rng2.popFront(); 103 | rng2.jump(); 104 | rng2.longJump(); 105 | assert(rng0.front != rng2.front, "longJump() should mutate front."); 106 | 107 | rng0.longJump(); 108 | assert(rng0.front == rng2.front, 109 | "Both rng0 and rng2 call 1 longJump + 1 jump + 1 popFront in total."); 110 | 111 | 112 | assert(Xorshiro128Plus.init.s == Xorshiro128Plus(0).s, 113 | "Zero seeded states should be equal to the default-initialized ones."); 114 | 115 | assert(Xorshiro128Plus.init.s != Xorshiro128Plus(1).s, 116 | "Non-zero states should be different from the default-initialized ones."); 117 | } 118 | -------------------------------------------------------------------------------- /source/synth2/ringbuffer.d: -------------------------------------------------------------------------------- 1 | module synth2.ringbuffer; 2 | 3 | import core.memory : pureFree, pureRealloc; 4 | 5 | /// nogc nothrow ringbufer. 6 | /// Params: 7 | /// T = element type. 8 | struct RingBuffer(T) { 9 | 10 | /// Reallocates memory. 11 | /// Params: 12 | /// n = #elements (not #bytes). 13 | void recalloc(size_t n) { 14 | if (n == _capacity) return; 15 | _ptr = cast(T*) pureRealloc(_ptr, n * T.sizeof); 16 | assert(_ptr, "realloc failed"); 17 | _capacity = n; 18 | this.clear(); 19 | } 20 | 21 | /// Clears payload. 22 | void clear() { 23 | _ptr[0 .. _capacity] = 0; 24 | } 25 | 26 | ~this() { pureFree(_ptr); } 27 | 28 | /// Returns: the head element. 29 | T front() const { return _ptr[_front_idx]; } 30 | 31 | /// Enqueues a new value. 32 | /// Params: 33 | /// val = new element. 34 | void enqueue(T val) { 35 | _ptr[_back_idx] = val; 36 | _back_idx = (_back_idx + 1) % _capacity; 37 | _front_idx = (_front_idx + 1) % _capacity; 38 | } 39 | 40 | /// Resizes the buffer. Initializes values to 0 if newlen > capacity. 41 | /// Params: 42 | /// newlen = new length. 43 | void resize(size_t newlen) { 44 | assert(newlen <= _capacity, "capacity exceeded"); 45 | // Ignore newlen in release mode. 46 | if (newlen > _capacity) { 47 | newlen = _capacity; 48 | } 49 | _front_idx = newlen < _back_idx 50 | ? _back_idx - newlen 51 | : _capacity - (newlen - _back_idx); 52 | } 53 | 54 | /// Returns: buffer length. 55 | size_t length() const { 56 | return _front_idx < _back_idx 57 | ? _back_idx - _front_idx 58 | : _capacity + _back_idx - _front_idx; 59 | } 60 | 61 | private: 62 | T* _ptr; 63 | size_t _capacity, _front_idx, _back_idx; 64 | } 65 | 66 | @nogc nothrow pure 67 | unittest { 68 | RingBuffer!float buf; 69 | buf.recalloc(2); 70 | buf.resize(2); 71 | assert(buf.length == 2); 72 | assert(buf.front == 0); 73 | 74 | buf.enqueue(1); 75 | assert(buf.front == 0); 76 | assert(buf.length == 2); 77 | 78 | buf.enqueue(2); 79 | assert(buf.front == 1); 80 | assert(buf.length == 2); 81 | 82 | // resize shorter 83 | buf.resize(1); 84 | assert(buf.length == 1); 85 | assert(buf.front == 2); 86 | 87 | // resize longer 88 | buf.resize(2); 89 | assert(buf.length == 2); 90 | assert(buf.front == 1); // previous front 91 | } 92 | -------------------------------------------------------------------------------- /source/synth2/voice.d: -------------------------------------------------------------------------------- 1 | /** 2 | Synth2 voice module. 3 | 4 | Copyright: klknn 2021. 5 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module synth2.voice; 8 | 9 | import mir.math.common : fastmath; 10 | 11 | import synth2.envelope : ADSR; 12 | 13 | /// Voice stack for storing previous voices for legato. 14 | struct VoiceStack { 15 | @nogc nothrow @safe pure: 16 | 17 | /// Returns: true if no voice is stacked. 18 | bool empty() const { return idx < 0; } 19 | 20 | /// Stacks new note. 21 | /// Params: 22 | /// note = MIDI note number [0, 127]. 23 | void push(int note) { 24 | if (idx + 1 == data.length) return; 25 | data[++idx] = note; 26 | on[note] = true; 27 | } 28 | 29 | /// Returns: the last played MIDI note number. 30 | int front() const { 31 | // TODO: assert(!empty); 32 | return empty ? data[0] : data[idx]; 33 | } 34 | 35 | /// Resets stack. 36 | void reset() { 37 | idx = -1; 38 | on[] = false; 39 | } 40 | 41 | /// Pops the last MIDI note from stack. 42 | void popFront() pure { 43 | while (!empty) { 44 | --idx; 45 | if (on[this.front]) return; 46 | } 47 | } 48 | 49 | private: 50 | int[128] data; 51 | bool[128] on; 52 | int idx; 53 | 54 | } 55 | 56 | /// Mono voice status. 57 | struct VoiceStatus { 58 | @nogc nothrow @safe @fastmath: 59 | 60 | bool isPlaying() const pure { 61 | return !_envelope.empty; 62 | } 63 | 64 | float front() const pure { 65 | if (!this.isPlaying) return 0f; 66 | return _gain * _envelope.front; 67 | } 68 | 69 | void popFront() pure { 70 | _envelope.popFront(); 71 | if (_legatoFrames < _portamentFrames) ++_legatoFrames; 72 | } 73 | 74 | void setSampleRate(float sampleRate) pure { 75 | _sampleRate = sampleRate; 76 | _envelope.setSampleRate(sampleRate); 77 | _legatoFrames = 0; 78 | _notePrev = -1; 79 | } 80 | 81 | void setParams(bool legato, float portament, bool autoPortament) pure { 82 | _legato = legato; 83 | _portamentFrames = portament * _sampleRate; 84 | _autoPortament = autoPortament; 85 | } 86 | 87 | void play(int note, float gain) pure { 88 | _notePrev = (_autoPortament && !this.isPlaying) ? -1 : _stack.front; 89 | _gain = gain; 90 | _legatoFrames = 0; 91 | _stack.push(note); 92 | if (_legato && this.isPlaying) return; 93 | _envelope.attack(); 94 | } 95 | 96 | void stop(int note) pure { 97 | if (this.isPlaying) { 98 | _stack.on[note] = false; 99 | if (_stack.front == note) { 100 | _stack.popFront(); 101 | _legatoFrames = 0; 102 | _notePrev = note; 103 | if (_legato && !_stack.empty) return; 104 | _envelope.release(); 105 | _stack.reset(); 106 | } 107 | } 108 | } 109 | 110 | float note() const pure { 111 | if (!_legato || _legatoFrames >= _portamentFrames 112 | || _notePrev == -1) return _stack.front; 113 | 114 | auto diff = (_stack.front - _notePrev) * _legatoFrames / _portamentFrames; 115 | return _notePrev + diff; 116 | } 117 | 118 | void setADSR(float a, float d, float s, float r) pure { 119 | _envelope.attackTime = a; 120 | _envelope.decayTime = d; 121 | _envelope.sustainLevel = s; 122 | _envelope.releaseTime = r; 123 | } 124 | 125 | private: 126 | float _sampleRate = 44_100; 127 | float _notePrev = -1; 128 | float _gain = 1f; 129 | 130 | bool _legato = false; 131 | bool _autoPortament = false; 132 | float _portamentFrames = 0; 133 | float _legatoFrames = 0; 134 | 135 | ADSR _envelope; 136 | VoiceStack _stack; 137 | } 138 | 139 | /// Test stacked previous notes used in legato. 140 | @nogc nothrow @safe pure 141 | unittest { 142 | VoiceStatus v; 143 | v._legato = true; 144 | v.play(23, 1); 145 | assert(v.note == 23); 146 | v.play(24, 1); 147 | assert(v.note == 24); 148 | v.stop(24); 149 | assert(v.note == 23); 150 | } 151 | -------------------------------------------------------------------------------- /source/synth2/waveform.d: -------------------------------------------------------------------------------- 1 | /** 2 | Waveform module. 3 | 4 | Copyright: klknn 2021. 5 | License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 | */ 7 | module synth2.waveform; 8 | 9 | import mir.math : sin, PI, M_1_PI, M_2_PI, fmin, fastmath, fma; 10 | 11 | import synth2.random : Xorshiro128Plus; 12 | 13 | /// Waveform kind. 14 | enum Waveform { 15 | sine, 16 | saw, 17 | pulse, 18 | triangle, 19 | noise, 20 | } 21 | 22 | /// String names of waveforms. 23 | static immutable waveformNames = [__traits(allMembers, Waveform)]; 24 | 25 | /// Waveform range. 26 | struct WaveformRange { 27 | @fastmath @safe nothrow @nogc pure: 28 | 29 | /// Infinite range method. 30 | enum empty = false; 31 | 32 | /// Returns: the current wave value. 33 | float front() const { 34 | final switch (this.waveform) { 35 | case Waveform.saw: 36 | return fma(- M_1_PI, this.phase, 1f); 37 | case Waveform.sine: 38 | return sin(this.phase); 39 | case Waveform.pulse: 40 | return this.phase <= this.pulseWidth * 2 * PI ? 1f : -1f; 41 | case Waveform.triangle: 42 | return fma(M_2_PI, fmin(this.phase, 2 * PI - this.phase), -1f); 43 | case Waveform.noise: 44 | return fma(2f / uint.max, cast(float) this.rng.front, - 1f); 45 | } 46 | } 47 | 48 | /// Increments timestamp of osc. 49 | /// Params: 50 | /// n = #frames. 51 | void popFront(long n = 1) { 52 | if (this.waveform == Waveform.noise) { 53 | this.rng.popFront(); 54 | return; 55 | } 56 | 57 | this.phase += this.freq * 2 * PI / this.sampleRate * n; 58 | this.normalized = false; 59 | if (this.phase >= 2 * PI) { 60 | this.phase %= 2 * PI; 61 | this.normalized = true; 62 | } 63 | } 64 | 65 | /// 66 | float freq = 440; 67 | /// 68 | float sampleRate = 44_100; 69 | /// 70 | Waveform waveform = Waveform.sine; 71 | /// 72 | float phase = 0; // [0 .. 2 * PI] 73 | /// 74 | float normalized = false; 75 | /// 76 | float pulseWidth = 0.5; 77 | 78 | private: 79 | Xorshiro128Plus rng = Xorshiro128Plus(0); 80 | } 81 | 82 | /// 83 | @safe nothrow @nogc pure unittest { 84 | import std.range; 85 | assert(isInputRange!WaveformRange); 86 | assert(isInfinite!WaveformRange); 87 | 88 | WaveformRange w; 89 | w.waveform = Waveform.noise; 90 | foreach (_; 0 .. 10) { 91 | assert(-1 < w.front && w.front < 1); 92 | w.popFront(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tool/filter_coeff.py: -------------------------------------------------------------------------------- 1 | # you need: pip install sympy== 1.7.1 2 | from sympy import * 3 | 4 | s = Symbol('s') 5 | z = Symbol('z') 6 | Q = Symbol('Q') # resonance 7 | T = Symbol('T') # sampling interval 8 | w0 = Symbol('w0') # cutoff freq 9 | 10 | # z2s = 2 / T * (z - 1) / (z + 1) 11 | s2z = 2 / T * (z - 1) / (z + 1) 12 | 13 | def tod(e): 14 | """Converts Python expr to D.""" 15 | return repr(e).replace("**", "^^") + ";" 16 | 17 | def print_coeff(hs): 18 | hz = simplify(hs.subs(s, s2z)) # Z transform 19 | npole = degree(denom(hs), s) 20 | print(" // === Transfer function ===") 21 | print(" // H(s) =", tod(hs)) # transfer function in Laplace domain 22 | print(" // H(z) =", tod(hz)) # transfer function in Z domain 23 | print(" // #pole =", npole) 24 | print(" // === Filter coeffients ===") 25 | print(f" nFIR = {npole + 1};") 26 | print(f" nIIR = {npole};") 27 | # FIR coeff 28 | dhz = collect(expand(denom(hz) * z ** -npole), z) 29 | nhz = collect(expand(numer(hz) * z ** -npole), z) 30 | a0 = dhz.coeff(z, 0) # to normalize a0 = 1 31 | for i in range(npole + 1): 32 | print(f" b[{i}] =", tod(nhz.coeff(z, -i) / a0)) 33 | # IIR coeff 34 | for i in range(1, npole + 1): 35 | print(f" a[{i-1}] =", tod(dhz.coeff(z, -i) / a0)) 36 | print(" return;") 37 | 38 | print("// -*- mode: d -*-") 39 | print("// DON'T MODIFY THIS FILE AS GENERATED BY tools/filter_coeff.py.") 40 | print() 41 | 42 | print("case FilterKind.LP6:") 43 | print_coeff(hs = 1 / (s / w0 + 1)) 44 | print() 45 | 46 | print("case FilterKind.HP6:") 47 | print_coeff(hs = s / (s + w0)) 48 | print() 49 | 50 | print("case FilterKind.LP12:") 51 | print_coeff(hs = 1 / (s**2 / w0**2 + s / w0 / Q + 1)) 52 | print() 53 | 54 | print("case FilterKind.HP12:") 55 | print_coeff(hs = (s**2 / w0**2) / (s**2 / w0**2 + s / w0 / Q + 1)) 56 | print() 57 | 58 | print("case FilterKind.BP12:") 59 | print_coeff(hs = (s / w0 / Q) / (s**2 / w0**2 + s / w0 / Q + 1)) 60 | print() 61 | 62 | print("case FilterKind.LP24:") 63 | print(" // Defined in VAFD Sec 5.1, Eq 5.1.") 64 | print(" // https://www.discodsp.net/VAFilterDesign_2.1.0.pdf") 65 | # print_coeff(hs = 1 / (s**2 / w0**2 + s / w0 / Q + 1)**2) 66 | print_coeff(hs = 1 / expand((Q + (1 + s / w0) ** 4))) 67 | print() 68 | 69 | print("case FilterKind.LPDL:") 70 | print(" // Defined in VAFD Sec 5.10, Eq 5.29.") 71 | print(" // https://www.discodsp.net/VAFilterDesign_2.1.0.pdf") 72 | print_coeff(hs = 1 / expand(8 * (1 + s / w0)**4 - 8 * (1 + s/w0)**2 + 1 + Q)) 73 | print() 74 | --------------------------------------------------------------------------------