├── resource ├── module-vst.lst ├── TFB.ttf ├── epiano.wav ├── module-au.lst ├── module-lv2.lst ├── FORCED SQUARE.ttf ├── module-vst.ver ├── module-vst3.lst ├── LeroyLetteringLightBeta01.ttf ├── module-vst3.ver ├── module-lv2.ver ├── BUILD ├── dub_upgrade.sh ├── build.sh ├── filter_coeff.py └── filter_coeff.d ├── dub.selections.json ├── bin ├── comp1 │ ├── dub.selections.json │ ├── comp1.d │ ├── plugin.json │ └── dub.json ├── envtool │ ├── dub.selections.json │ ├── envtool.d │ ├── plugin.json │ └── dub.json ├── epiano2 │ ├── dub.selections.json │ ├── main.d │ ├── plugin.json │ └── dub.json ├── freeverb │ ├── dub.selections.json │ ├── plugin.json │ ├── dub.json │ └── main.d └── synth2 │ ├── dub.selections.json │ ├── main.d │ ├── plugin.json │ └── dub.json ├── third_party ├── BUILD.intel-intrinsics ├── BUILD.mir-core └── BUILD.Dplug ├── .gitmodules ├── .gitignore ├── WORKSPACE ├── dub.json ├── LICENSE ├── .github └── workflows │ ├── bazel.yml │ ├── codecov.yml │ └── ci.yml ├── source └── kdr │ ├── audiofmt.d │ ├── hibiki │ └── client.d │ ├── equalizer.d │ ├── simplegui.d │ ├── testing.d │ ├── modfilter.d │ ├── delay.d │ ├── params.d │ ├── waveform.d │ ├── ringbuffer.d │ ├── chorus.d │ ├── BUILD │ ├── epiano2 │ ├── parameter.d │ └── client.d │ ├── random.d │ ├── voice.d │ ├── envtool │ ├── client.d │ ├── params.d │ └── gui.d │ ├── logging.d │ ├── filter.d │ ├── lfo.d │ ├── compressor.d │ ├── oscillator.d │ ├── effect.d │ ├── envelope.d │ └── synth2 │ └── params.d ├── rules ├── d_toolchain.bzl ├── BUILD ├── dmd.bzl ├── ldc2.bzl └── d.bzl ├── README.md └── dscanner.ini /resource/module-vst.lst: -------------------------------------------------------------------------------- 1 | _VSTPluginMain 2 | _main_macho 3 | -------------------------------------------------------------------------------- /resource/TFB.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/kdr/HEAD/resource/TFB.ttf -------------------------------------------------------------------------------- /resource/epiano.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/kdr/HEAD/resource/epiano.wav -------------------------------------------------------------------------------- /resource/module-au.lst: -------------------------------------------------------------------------------- 1 | _dplugAUEntryPoint 2 | _dplugAUComponentFactoryFunction 3 | -------------------------------------------------------------------------------- /resource/module-lv2.lst: -------------------------------------------------------------------------------- 1 | _GenerateManifestFromClient 2 | _lv2_descriptor 3 | _lv2ui_descriptor -------------------------------------------------------------------------------- /resource/FORCED SQUARE.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/kdr/HEAD/resource/FORCED SQUARE.ttf -------------------------------------------------------------------------------- /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/LeroyLetteringLightBeta01.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klknn/kdr/HEAD/resource/LeroyLetteringLightBeta01.ttf -------------------------------------------------------------------------------- /resource/module-vst3.ver: -------------------------------------------------------------------------------- 1 | VST3ABI_1.0 { 2 | global: GetPluginFactory; ModuleEntry; ModuleExit; 3 | local: *; 4 | }; 5 | -------------------------------------------------------------------------------- /resource/module-lv2.ver: -------------------------------------------------------------------------------- 1 | LV2ABI_1.0 { 2 | global: lv2_descriptor; lv2ui_descriptor; GenerateManifestFromClient; 3 | local: *; 4 | }; 5 | -------------------------------------------------------------------------------- /resource/BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | filegroup( 4 | name = "filter_coeff", 5 | srcs = ["filter_coeff.d"], 6 | ) 7 | -------------------------------------------------------------------------------- /resource/dub_upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | dub upgrade 6 | 7 | for d in $(ls bin) 8 | do 9 | cd "bin/${d}" 10 | dub upgrade 11 | cd - 12 | done 13 | -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "dplug": "13.6.0", 5 | "gamut": "2.1.4", 6 | "intel-intrinsics": "1.11.9", 7 | "mir-core": "1.5.5" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /bin/comp1/dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "dplug": "13.6.0", 5 | "gamut": "2.1.4", 6 | "intel-intrinsics": "1.11.9", 7 | "kdr": {"path":"../.."}, 8 | "mir-core": "1.5.5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /bin/envtool/dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "dplug": "13.6.0", 5 | "gamut": "2.1.4", 6 | "intel-intrinsics": "1.11.9", 7 | "kdr": {"path":"../.."}, 8 | "mir-core": "1.5.5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /bin/epiano2/dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "dplug": "13.6.0", 5 | "gamut": "2.1.4", 6 | "intel-intrinsics": "1.11.9", 7 | "kdr": {"path":"../.."}, 8 | "mir-core": "1.5.5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /bin/freeverb/dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "dplug": "13.6.0", 5 | "gamut": "2.1.4", 6 | "intel-intrinsics": "1.11.9", 7 | "kdr": {"path":"../.."}, 8 | "mir-core": "1.5.5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /bin/synth2/dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "dplug": "13.6.0", 5 | "gamut": "2.1.4", 6 | "intel-intrinsics": "1.11.9", 7 | "kdr": {"path":"../.."}, 8 | "mir-core": "1.5.5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /resource/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | for d in $(ls bin) 6 | do 7 | if [ "${d}" = "comp1" ] 8 | then 9 | continue 10 | fi 11 | cd "bin/${d}" 12 | dub run dplug:dplug-build -b=release -- --final -c VST3 13 | cd - 14 | done 15 | -------------------------------------------------------------------------------- /bin/comp1/comp1.d: -------------------------------------------------------------------------------- 1 | import dplug.core; 2 | import dplug.client; 3 | 4 | class ClientWithInfo : dplug.client.Client { 5 | override PluginInfo buildPluginInfo() { 6 | static immutable info = parsePluginInfo(import("plugin.json")); 7 | return info; 8 | } 9 | } 10 | 11 | mixin(pluginEntryPoints!ClientWithInfo); 12 | -------------------------------------------------------------------------------- /third_party/BUILD.intel-intrinsics: -------------------------------------------------------------------------------- 1 | # -*- mode: bazel-build -*- 2 | package(default_visibility = ["//visibility:public"]) 3 | 4 | load("@//rules:d.bzl", "d_library") 5 | 6 | d_library( 7 | name = "intel-intrinsics", 8 | srcs = glob(["source/inteli/*.d"]), 9 | imports = ["source"], 10 | ) 11 | 12 | # TODO(klknn): Add tests. 13 | -------------------------------------------------------------------------------- /bin/epiano2/main.d: -------------------------------------------------------------------------------- 1 | import dplug.client; 2 | import dplug.vst3; 3 | 4 | import kdr.epiano2.client : Epiano2Client; 5 | 6 | class ClientWithInfo : Epiano2Client { 7 | override PluginInfo buildPluginInfo() { 8 | static immutable info = parsePluginInfo(import("plugin.json")); 9 | return info; 10 | } 11 | } 12 | 13 | mixin(pluginEntryPoints!ClientWithInfo); 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/mir-core"] 2 | path = third_party/mir-core 3 | url = https://github.com/libmir/mir-core 4 | [submodule "third_party/Dplug"] 5 | path = third_party/Dplug 6 | url = https://github.com/AuburnSounds/Dplug 7 | [submodule "third_party/intel-intrinsics"] 8 | path = third_party/intel-intrinsics 9 | url = https://github.com/AuburnSounds/intel-intrinsics 10 | -------------------------------------------------------------------------------- /bin/envtool/envtool.d: -------------------------------------------------------------------------------- 1 | import dplug.client; 2 | 3 | import kdr.envtool.client : EnvToolClient; 4 | 5 | 6 | /// 7 | class ClientWithInfo : EnvToolClient { 8 | override PluginInfo buildPluginInfo() { 9 | // Plugin info is parsed from plugin.json here at compile time. 10 | static immutable info = parsePluginInfo(import("plugin.json")); 11 | return info; 12 | } 13 | } 14 | 15 | 16 | mixin(pluginEntryPoints!ClientWithInfo); 17 | -------------------------------------------------------------------------------- /bin/synth2/main.d: -------------------------------------------------------------------------------- 1 | import dplug.client; 2 | import kdr.synth2.client : Synth2Client; 3 | 4 | 5 | /// 6 | class Synth2ClientWithInfo : Synth2Client { 7 | override PluginInfo buildPluginInfo() { 8 | // Plugin info is parsed from plugin.json here at compile time. 9 | static immutable info = parsePluginInfo(import("plugin.json")); 10 | return info; 11 | } 12 | } 13 | 14 | 15 | mixin(pluginEntryPoints!Synth2ClientWithInfo); 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !*/ 4 | !*.d 5 | !*.ini 6 | !*.ipynb 7 | !*.json 8 | !*.md 9 | !*.py 10 | !*.bzl 11 | !*.sh 12 | !BUILD 13 | !WORKSPACE 14 | !.gitignore 15 | !LICENSE 16 | 17 | !source/ 18 | !bin/ 19 | !rules/ 20 | !third_party/* 21 | third_party/*~ 22 | 23 | !resource/ 24 | !resource/*.ver 25 | !resource/*.lst 26 | !resource/*.png 27 | !resource/*.ttf 28 | !resource/*.wav 29 | 30 | !.github/ 31 | !.github/workflows/ 32 | !.github/workflows/*.yml 33 | 34 | builds/ 35 | .* -------------------------------------------------------------------------------- /bin/comp1/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/AuburnSounds/dplug/master/plugin-schema.json", 3 | "vendorName": "kdr", 4 | "vendorUniqueID": "KDR_", 5 | "vendorSupportEmail": "klknn.gh@gmail.com", 6 | "pluginName": "kdr-comp1", 7 | "pluginHomepage": "https://github.com/klknn/kdr", 8 | "pluginUniqueID": "kcm1", 9 | "publicVersion": "1.0.0", 10 | "CFBundleIdentifierPrefix": "com.kdr", 11 | "hasGUI": false, 12 | "isSynth": false, 13 | "receivesMIDI": true, 14 | "category": "effectModulation" 15 | } 16 | -------------------------------------------------------------------------------- /bin/envtool/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/AuburnSounds/dplug/master/plugin-schema.json", 3 | "vendorName": "kdr", 4 | "vendorUniqueID": "KDR_", 5 | "vendorSupportEmail": "klknn.gh@gmail.com", 6 | "pluginName": "kdr-envtool", 7 | "pluginHomepage": "https://github.com/klknn/kdr", 8 | "pluginUniqueID": "kenv", 9 | "publicVersion": "1.0.0", 10 | "CFBundleIdentifierPrefix": "com.kdr", 11 | "hasGUI": true, 12 | "isSynth": false, 13 | "receivesMIDI": true, 14 | "category": "effectModulation" 15 | } 16 | -------------------------------------------------------------------------------- /bin/freeverb/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/AuburnSounds/dplug/master/plugin-schema.json", 3 | "vendorName": "kdr", 4 | "vendorUniqueID": "KDR_", 5 | "vendorSupportEmail": "klknn.gh@gmail.com", 6 | "pluginName": "kdr-freeverb", 7 | "pluginHomepage": "https://github.com/klknn/kdr", 8 | "pluginUniqueID": "kfrb", 9 | "publicVersion": "1.0.0", 10 | "CFBundleIdentifierPrefix": "com.kdr", 11 | "hasGUI": false, 12 | "isSynth": false, 13 | "receivesMIDI": true, 14 | "category": "effectReverb" 15 | } 16 | -------------------------------------------------------------------------------- /bin/synth2/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/AuburnSounds/dplug/master/plugin-schema.json", 3 | "vendorName": "kdr", 4 | "vendorUniqueID": "KDR_", 5 | "vendorSupportEmail": "klknn.gh@gmail.com", 6 | "pluginName": "kdr-synth2", 7 | "pluginHomepage": "https://github.com/klknn/kdr", 8 | "pluginUniqueID": "ksy2", 9 | "publicVersion": "1.0.0", 10 | "CFBundleIdentifierPrefix": "com.kdr", 11 | "hasGUI": true, 12 | "isSynth": true, 13 | "receivesMIDI": true, 14 | "category": "instrumentSynthesizer" 15 | } 16 | -------------------------------------------------------------------------------- /bin/epiano2/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/AuburnSounds/dplug/master/plugin-schema.json", 3 | "vendorName": "kdr", 4 | "vendorUniqueID": "KDR_", 5 | "vendorSupportEmail": "klknn.gh@gmail.com", 6 | "pluginName": "kdr-epiano2", 7 | "pluginHomepage": "https://github.com/klknn/kdr", 8 | "pluginUniqueID": "kep2", 9 | "publicVersion": "1.0.0", 10 | "CFBundleIdentifierPrefix": "com.kdr", 11 | "hasGUI": false, 12 | "isSynth": true, 13 | "receivesMIDI": true, 14 | "category": "instrumentSynthesizer" 15 | } 16 | -------------------------------------------------------------------------------- /third_party/BUILD.mir-core: -------------------------------------------------------------------------------- 1 | # -*- mode: bazel-build -*- 2 | package(default_visibility = ["//visibility:public"]) 3 | 4 | load("@//rules:d.bzl", "d_library") 5 | 6 | d_library( 7 | name = "internal", 8 | srcs = glob(["source/mir/internal/*.d"]), 9 | imports = ["source"], 10 | ) 11 | 12 | d_library( 13 | name = "math", 14 | srcs = glob(["source/mir/math/*.d"]), 15 | deps = [":internal"], 16 | imports = ["source"], 17 | ) 18 | 19 | d_library( 20 | name = "complex", 21 | srcs = glob(["source/mi/complex/*.d"]), 22 | deps = [":math"], 23 | imports = ["source"], 24 | ) 25 | 26 | # TODO(klknn): Add all the modules and tests. 27 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 2 | load("//rules:dmd.bzl", "dmd_repositories") 3 | load("//rules:ldc2.bzl", "ldc2_repositories") 4 | 5 | dmd_repositories() 6 | ldc2_repositories() 7 | 8 | new_local_repository( 9 | name = "mir-core", 10 | path = "third_party/mir-core", 11 | build_file = "third_party/BUILD.mir-core", 12 | ) 13 | 14 | new_local_repository( 15 | name = "intel-intrinsics", 16 | path = "third_party/intel-intrinsics", 17 | build_file = "third_party/BUILD.intel-intrinsics", 18 | ) 19 | 20 | new_local_repository( 21 | name = "Dplug", 22 | path = "third_party/Dplug", 23 | build_file = "third_party/BUILD.Dplug", 24 | ) 25 | -------------------------------------------------------------------------------- /third_party/BUILD.Dplug: -------------------------------------------------------------------------------- 1 | # -*- mode: bazel-build -*- 2 | package(default_visibility = ["//visibility:public"]) 3 | 4 | load("@//rules:d.bzl", "d_library") 5 | 6 | d_library( 7 | name = "core", 8 | srcs = glob(["core/dplug/core/*.d"]), 9 | imports = ["core"], 10 | deps = ["@intel-intrinsics//:intel-intrinsics"], 11 | ) 12 | 13 | d_library( 14 | name = "math", 15 | srcs = glob(["math/dplug/math/*.d"]), 16 | imports = ["math"], 17 | deps = ["@intel-intrinsics//:intel-intrinsics"], 18 | ) 19 | 20 | d_library( 21 | name = "client", 22 | srcs = glob(["client/dplug/client/*.d"]), 23 | imports = ["client"], 24 | deps = [":core"], 25 | ) 26 | 27 | 28 | # TODO(klknn): Add all the modules and tests. 29 | -------------------------------------------------------------------------------- /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 | "dependencies": { 20 | "dplug:flat-widgets": "~>13.0", 21 | "dplug:pbr-widgets": "~>13.0", 22 | "mir-core": "~>1.3" 23 | }, 24 | "name": "kdr", 25 | "stringImportPaths": [ 26 | "resource" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/bazel.yml: -------------------------------------------------------------------------------- 1 | name: bazel 2 | 3 | on: 4 | push: 5 | # Nightly builds 6 | schedule: 7 | - cron: '00 00 * * *' 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | - macos-latest 18 | # TODO(klknn): Support Windows cache 19 | # https://qiita.com/homulerdora/items/12745a02a2663bc956fd#%E8%A7%A3%E6%B1%BA%E7%AD%96 20 | # - windows-latest 21 | arch: 22 | - 'x86_64' 23 | compiler: 24 | - 'ldc2' 25 | - 'dmd' 26 | 27 | steps: 28 | - name: Cache bazel 29 | uses: actions/cache@v3 30 | env: 31 | cache-name: bazel-cache 32 | with: 33 | path: | 34 | ~/.cache/bazelisk 35 | ~/.cache/bazel 36 | key: ${{ matrix.os }}-${{ matrix.compiler }}-${{ env.cache-name }} 37 | 38 | - uses: actions/checkout@v3 39 | with: 40 | submodules: 'recursive' 41 | 42 | - name: Test 43 | # TODO(klknn): Fix dmd+macos linker errors: 44 | # https://github.com/klknn/kdr/actions/runs/3382812329/jobs/5618111876 45 | if: (startsWith(matrix.compiler,'dmd') && startsWith(matrix.os,'macos')) != true 46 | run: | 47 | bazel test --test_output=all --test_verbose_timeout_warnings \ 48 | $(bazel query //...) --//rules:d_compiler=${{ matrix.compiler }} 49 | -------------------------------------------------------------------------------- /.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-latest 24 | - windows-latest 25 | arch: 26 | - 'x86_64' 27 | compiler: 28 | - 'ldc-latest' 29 | - 'dmd-latest' 30 | steps: 31 | - name: Checkout master branch 32 | uses: actions/checkout@v3 33 | 34 | - name: Install Dependencies - Ubuntu 35 | if: startsWith(matrix.os,'ubuntu') 36 | run: | 37 | sudo apt-get -yq install libx11-dev 38 | 39 | - name: Install compiler 40 | uses: dlang-community/setup-dlang@v1 41 | with: 42 | compiler: ${{ matrix.compiler }} 43 | 44 | # Disabled for false alarms on const, and bugs in its module exclusion. 45 | # - name: D-Scanner 46 | # run: dub fetch dscanner && dub run dscanner -- --styleCheck source bin 47 | 48 | - name: Test 49 | run: | 50 | dub test -b=unittest-cov 51 | 52 | - uses: codecov/codecov-action@v3 53 | 54 | Skip: 55 | if: "contains(github.event.head_commit.message, '[skip ci]')" 56 | runs-on: ubuntu-20.04 57 | steps: 58 | - name: Skip CI 🚫 59 | run: echo skip CI 60 | -------------------------------------------------------------------------------- /source/kdr/audiofmt.d: -------------------------------------------------------------------------------- 1 | /// Audio formats. 2 | module kdr.audiofmt; 3 | 4 | /// http://soundfile.sapp.org/doc/WaveFormat/ 5 | struct Wav { 6 | @nogc nothrow: 7 | 8 | struct Header { 9 | char[4] riffId; 10 | int chunkSize; 11 | char[4] waveId; 12 | char[4] fmtId; 13 | int fmtSize; 14 | short fmtCode; 15 | short numChannels; 16 | int sampleRate; 17 | int bytePerSecond; 18 | short blockBoundary; 19 | short bitPerSample; 20 | char[4] dataId; 21 | int fileSize; 22 | } 23 | 24 | const(Header)* header; 25 | const(void)* ptr; 26 | 27 | alias header this; 28 | 29 | this(const(void)[] bytes) { 30 | this.header = cast(const(Header)*) bytes.ptr; 31 | this.ptr = bytes.ptr + Header.sizeof; 32 | 33 | assert(riffId == "RIFF"); 34 | assert(waveId == "WAVE"); 35 | assert(fmtId == "fmt "); 36 | assert(dataId == "data"); 37 | } 38 | 39 | const(T)[] data(T = short)() const { 40 | assert(T.sizeof * 8 == bitPerSample); 41 | auto p = cast(T*) this.ptr; 42 | return p[0 .. this.fileSize / (T.sizeof / byte.sizeof)]; 43 | } 44 | } 45 | 46 | unittest { 47 | Wav wav = Wav(import("epiano.wav")); 48 | with (wav) { 49 | assert(fmtSize == 16); 50 | assert(fmtCode == 1); 51 | assert(numChannels == 1); 52 | assert(sampleRate == 44100); 53 | assert(bytePerSecond == sampleRate * blockBoundary); 54 | assert(blockBoundary == 2); 55 | assert(bitPerSample == 16); 56 | assert(data!short.length == 422418); 57 | assert(data!short[0] == -7); 58 | assert(data!short[$-1] == 0); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /source/kdr/hibiki/client.d: -------------------------------------------------------------------------------- 1 | module kdr.hibiki.client; 2 | 3 | import std.math; 4 | import dplug.core; 5 | import dplug.client; 6 | import kdr.params; 7 | 8 | /// Plugin parameter IDs. 9 | @RegisterBuilder!ParamBuilder 10 | enum Params { 11 | onOff, 12 | } 13 | 14 | /// Plugin parameter definitions. 15 | struct ParamBuilder { 16 | /// Returns: bool on/off switch. 17 | static onOff() { 18 | return mallocNew!BoolParameter(Params.onOff, "onOff", true); 19 | } 20 | } 21 | 22 | 23 | /// Reverb effect client. 24 | class HibikiClient : Client { 25 | public: 26 | nothrow: 27 | @nogc: 28 | 29 | /// ctor. 30 | this() 31 | { 32 | } 33 | 34 | override PluginInfo buildPluginInfo() 35 | { 36 | return PluginInfo.init; 37 | } 38 | 39 | override Parameter[] buildParameters() 40 | { 41 | return buildParams!Params; 42 | } 43 | 44 | override LegalIO[] buildLegalIO() 45 | { 46 | auto io = makeVec!LegalIO(); 47 | io ~= LegalIO(2, 2); 48 | return io.releaseData(); 49 | } 50 | 51 | override void reset(double sampleRate, int maxFrames, int numInputs, int numOutputs) nothrow @nogc 52 | { 53 | } 54 | 55 | override void processAudio(const(float*)[] inputs, float*[]outputs, int frames, TimeInfo info) nothrow @nogc 56 | { 57 | if (readParam!bool(Params.onOff)) { 58 | outputs[0][0..frames] = (inputs[0][0..frames] + inputs[1][0..frames]) * SQRT1_2; 59 | outputs[1][0..frames] = (inputs[0][0..frames] - inputs[1][0..frames]) * SQRT1_2; 60 | } else { 61 | outputs[0][0..frames] = inputs[0][0..frames]; 62 | outputs[1][0..frames] = inputs[1][0..frames]; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /source/kdr/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 kdr.equalizer; 8 | 9 | import mir.math.common : fabs, log, fmax, exp; 10 | 11 | import kdr.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 | -------------------------------------------------------------------------------- /rules/d_toolchain.bzl: -------------------------------------------------------------------------------- 1 | """D toolchain module unifying compilers e.g. DMD, LDC2. """ 2 | 3 | load(":dmd.bzl", "dmd_compile_attrs") 4 | load(":ldc2.bzl", "ldc2_compile_attrs") 5 | 6 | _DCompilerInfo = provider(fields = ["name"]) 7 | 8 | def _d_compiler_impl(ctx): 9 | return _DCompilerInfo(name = ctx.build_setting_value) 10 | 11 | d_compiler = rule( 12 | _d_compiler_impl, 13 | build_setting = config.string(flag = True) 14 | ) 15 | 16 | def d_toolchain(ctx): 17 | """Returns a struct containing info about the D toolchain. 18 | 19 | Args: 20 | ctx: The ctx object. 21 | 22 | Return: 23 | Struct containing D toolchain info: 24 | """ 25 | name = ctx.attr.d_compiler[_DCompilerInfo].name 26 | # print("D compiler selected by --//rules:d_compiler is: " + name) 27 | if name == "dmd": 28 | flag_version = ctx.attr._dmd_flag_version 29 | compiler = ctx.file._dmd_compiler 30 | runtime_import_src = ctx.files._dmd_runtime_import_src 31 | stdlib = ctx.files._dmd_stdlib 32 | stdlib_src = ctx.files._dmd_stdlib_src 33 | elif name == "ldc2": 34 | flag_version = ctx.attr._ldc2_flag_version 35 | compiler = ctx.file._ldc2_compiler 36 | runtime_import_src = ctx.files._ldc2_runtime_import_src 37 | stdlib = ctx.files._ldc2_stdlib 38 | stdlib_src = ctx.files._ldc2_stdlib_src 39 | else: 40 | # TODO(klknn): Support GDC. 41 | fail("unknown compiler: " + name) 42 | 43 | return struct( 44 | flag_version = flag_version, 45 | compiler = compiler, 46 | runtime_import_src = runtime_import_src, 47 | stdlib = stdlib, 48 | stdlib_src = stdlib_src, 49 | ) 50 | 51 | d_toolchain_attrs = dict( 52 | {"d_compiler": attr.label(default = ":d_compiler")}.items() + 53 | dmd_compile_attrs.items() + 54 | ldc2_compile_attrs.items() 55 | ) 56 | -------------------------------------------------------------------------------- /source/kdr/simplegui.d: -------------------------------------------------------------------------------- 1 | module kdr.simplegui; 2 | 3 | import dplug.core : mallocNew, destroyFree; 4 | import dplug.math : box2i, rectangle; 5 | import dplug.graphics : blitTo, cropImageRef, ImageRef, OwnedImage, RGBA, toRef, L16; 6 | import dplug.gui : flagPBR, flagAnimated, GUIGraphics, SizeConstraints, makeSizeConstraintsFixed; 7 | 8 | /// Minimalist GUI for PBR elements. 9 | class PBRSimpleGUI : GUIGraphics { 10 | public: 11 | @nogc nothrow: 12 | this(SizeConstraints size, RGBA color = RGBA(114, 114, 114, 0)) { 13 | super(size, flagPBR | flagAnimated); 14 | _color = color; 15 | } 16 | 17 | override void onDrawPBR( 18 | ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, 19 | ImageRef!RGBA materialMap, box2i[] dirtyRects) const pure { 20 | foreach(dirtyRect; dirtyRects) { 21 | fill(diffuseMap, dirtyRect, _color); 22 | fill(depthMap, dirtyRect, L16(0)); 23 | fill(materialMap, dirtyRect, RGBA(0, 0, 0, 0)); 24 | } 25 | } 26 | 27 | private: 28 | void fill(T)(ImageRef!T map, box2i dirtyRect, T color) const pure { 29 | ImageRef!T output = map.cropImageRef(dirtyRect); 30 | foreach (y; 0 .. output.h) { 31 | output.scanline(y)[0 .. $] = color; 32 | } 33 | } 34 | 35 | RGBA _color; 36 | } 37 | 38 | nothrow 39 | unittest { 40 | int w = 100, h = 100; 41 | RGBA color = RGBA(42, 42, 42, 42); 42 | auto gui = new PBRSimpleGUI(makeSizeConstraintsFixed(w, h), color); 43 | auto dif = new OwnedImage!RGBA(w, h); 44 | auto dep = new OwnedImage!L16(w, h); 45 | auto mat = new OwnedImage!RGBA(w, h); 46 | gui.onDrawPBR(toRef(dif), toRef(dep), toRef(mat), [rectangle(0, 0, w, h)]); 47 | 48 | assert(dif[0, 0] == color); 49 | assert(dif[w-1, h-1] == color); 50 | 51 | assert(dep[0, 0] == L16(0)); 52 | assert(dep[w-1, h-1] == L16(0)); 53 | 54 | assert(mat[0, 0] == RGBA(0, 0, 0, 0)); 55 | assert(mat[w-1, h-1] == RGBA(0, 0, 0, 0)); 56 | } 57 | -------------------------------------------------------------------------------- /source/kdr/testing.d: -------------------------------------------------------------------------------- 1 | module kdr.testing; 2 | 3 | import std.datetime.stopwatch : benchmark; 4 | 5 | import dplug.core; 6 | import dplug.client; 7 | 8 | import kdr.logging : logInfo; 9 | 10 | /// Mock host for testing a client. 11 | struct GenericTestHost(C) { 12 | C client; 13 | int frames = 8; 14 | Vec!float[2] inputFrames, outputFrames; 15 | MidiMessage msg1 = makeMidiMessageNoteOn(0, 0, 100, 100); 16 | MidiMessage msg2 = makeMidiMessageNoteOn(1, 0, 90, 10); 17 | MidiMessage msg3 = makeMidiMessageNoteOff(2, 0, 100); 18 | bool noteOff = false; 19 | 20 | @nogc nothrow: 21 | 22 | void processAudio() { 23 | inputFrames[0].resize(this.frames); 24 | inputFrames[1].resize(this.frames); 25 | outputFrames[0].resize(this.frames); 26 | outputFrames[1].resize(this.frames); 27 | client.reset(44_100, 32, 0, 2); 28 | 29 | float*[2] inputs, outputs; 30 | inputs[0] = &outputFrames[0][0]; 31 | inputs[1] = &inputFrames[1][0]; 32 | outputs[0] = &outputFrames[0][0]; 33 | outputs[1] = &outputFrames[1][0]; 34 | 35 | client.enqueueMIDIFromHost(msg1); 36 | client.enqueueMIDIFromHost(msg2); 37 | if (noteOff) { 38 | client.enqueueMIDIFromHost(msg3); 39 | } 40 | 41 | TimeInfo info; 42 | info.hostIsPlaying = true; 43 | client.processAudioFromHost(inputs[], outputs[], frames, info); 44 | } 45 | } 46 | 47 | /// Test default params with benchmark. 48 | void benchmarkWithDefaultParams(ClientImpl)(int timeoutMSec = 20) { 49 | GenericTestHost!ClientImpl host = { client: new ClientImpl(), frames: 100 }; 50 | 51 | host.processAudio(); // to omit the first record. 52 | auto time = benchmark!(() => host.processAudio())(100)[0].split!("msecs", "usecs"); 53 | logInfo("benchmark %s/default: %d ms %d us", ClientImpl.stringof.ptr, 54 | cast(int) time.msecs, cast(int) time.usecs); 55 | 56 | version (D_Coverage) {} 57 | else { 58 | version (OSX) {} 59 | else { 60 | version (LDC) assert(time.msecs <= timeoutMSec); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kdr: klknn dplug repo 2 | 3 | [⬇️ DOWNLOAD FREE PLUGINS ⬇️](https://github.com/klknn/kdr/releases) 4 | 5 | [![ci](https://github.com/klknn/kdr/actions/workflows/ci.yml/badge.svg)](https://github.com/klknn/kdr/actions/workflows/ci.yml) 6 | [![codecov](https://codecov.io/gh/klknn/kdr/branch/master/graph/badge.svg?token=4HMC5S2GER)](https://codecov.io/gh/klknn/kdr) 7 | 8 | ## How to build this plugin? 9 | 10 | https://github.com/AuburnSounds/Dplug/wiki/Getting-Started 11 | 12 | ## synth2 13 | 14 | virtual-analog synth like [synth1](https://www.kvraudio.com/product/synth1-by-daichi-laboratory-ichiro-toda) in D. 15 | 16 | Features (TODO) 17 | 18 | - [x] Multi-platform 19 | - [x] VST/VST3/AU CI build 20 | - [x] Windows/Linux CI test (macOS won't be tested because I don't have it) 21 | - [x] Oscillators 22 | - [x] sin/saw/square/triangle/noise waves 23 | - [x] 2nd/sub osc 24 | - [x] detune 25 | - [x] sync 26 | - [x] FM 27 | - [x] AM (ring) 28 | - [x] master control (keyshift/tune/phase/mix/PW) 29 | - [x] mod envelope 30 | - [x] Amplifier 31 | - [x] velocity sensitivity 32 | - [x] ADSR 33 | - [x] Filter 34 | - [x] HP6/HP12/LP6/LP12/LP24/LPDL(TB303 like filter) 35 | - [x] ADSR 36 | - [x] Saturation 37 | - [x] GUI 38 | - [x] LFO 39 | - [x] Effect (phaser is WIP) 40 | - [x] Equalizer / Pan 41 | - [x] Voice 42 | - [x] Tempo Delay 43 | - [x] Chorus / Flanger 44 | - [ ] Unison 45 | - [ ] Reverb 46 | - [ ] Arpeggiator 47 | - [ ] Presets 48 | - [ ] MIDI 49 | - [x] Pitch bend 50 | - [ ] Mod wheel 51 | - [ ] Control change 52 | - [ ] Program change 53 | 54 | ## envtool 55 | 56 | Envelope shaping effect for tremolo sidechain like kickstart or LFO tools. 57 | 58 | - [x] WYSWIG envelope edit 59 | - [x] Beat sync rate control 60 | - [x] Depth control 61 | - [x] LR offset control 62 | - [x] Volume mod 63 | - [ ] Filter mod 64 | - [ ] Pan mod 65 | - [ ] Presets 66 | 67 | ## reverb 68 | 69 | TBA 70 | 71 | ## History 72 | 73 | - 29 Dec 2022: Add envtool 74 | - 24 Sep 2022: Move from https://github.com/klknn/synth2 to https://github.com/klknn/kdr 75 | - 15 Feb 2021: Fork [poly-alias-synth](https://github.com/AuburnSounds/Dplug/tree/v10.2.1/examples/poly-alias-synth) for synth2. 76 | -------------------------------------------------------------------------------- /bin/comp1/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Pure-D/code-d/master/json-validation/dub.schema.json", 3 | 4 | "name": "comp1", 5 | 6 | "license": "BSL-1.0", 7 | "importPaths": [ "." ], 8 | "sourcePaths": [ "." ], 9 | "stringImportPaths": ["../../resource", "."], 10 | 11 | "dflags-linux-dmd": ["-defaultlib=libphobos2.a"], 12 | "dflags-osx-ldc": ["-static"], 13 | "dflags-linux-ldc": ["-link-defaultlib-shared=false"], 14 | "dflags-linux-x86_64-ldc": ["-fvisibility=hidden"], 15 | "dflags-windows-ldc": ["-mscrtlib=libcmt","-fvisibility=hidden", "-link-defaultlib-shared=false"], 16 | 17 | "dependencies": 18 | { 19 | "dplug:lv2": "~>13.0", 20 | "dplug:au": "~>13.0", 21 | "dplug:vst2": "~>13.0", 22 | "dplug:vst3": "~>13.0", 23 | "kdr": { "path": "../.." }, 24 | }, 25 | 26 | "configurations": [ 27 | { 28 | "name": "VST3", 29 | "versions": ["VST3"], 30 | "targetType": "dynamicLibrary", 31 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-vst3.lst", "-dead_strip" ], 32 | "lflags-linux-ldc": [ "--version-script=../../resource/module-vst3.ver" ] 33 | }, 34 | { 35 | "name": "VST2", 36 | "versions": ["VST2"], 37 | "targetType": "dynamicLibrary", 38 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-vst.lst", "-dead_strip" ], 39 | "lflags-linux-ldc": [ "--version-script=../../resource/module-vst.ver" ] 40 | }, 41 | { 42 | "name": "AU", 43 | "versions": ["AU"], 44 | "targetType": "dynamicLibrary", 45 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-au.lst", "-dead_strip" ] 46 | }, 47 | { 48 | "name": "LV2", 49 | "versions": ["LV2"], 50 | "targetType": "dynamicLibrary", 51 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-lv2.lst", "-dead_strip" ], 52 | "lflags-linux-ldc": [ "--version-script=../../resource/module-lv2.ver" ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /bin/synth2/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Pure-D/code-d/master/json-validation/dub.schema.json", 3 | 4 | "name": "synth2", 5 | 6 | "license": "BSL-1.0", 7 | "importPaths": [ "." ], 8 | "sourcePaths": [ "." ], 9 | "stringImportPaths": ["../../resource", "."], 10 | 11 | "dflags-linux-dmd": ["-defaultlib=libphobos2.a"], 12 | "dflags-osx-ldc": ["-static"], 13 | "dflags-linux-ldc": ["-link-defaultlib-shared=false"], 14 | "dflags-linux-x86_64-ldc": ["-fvisibility=hidden"], 15 | "dflags-windows-ldc": ["-mscrtlib=libcmt","-fvisibility=hidden", "-link-defaultlib-shared=false"], 16 | 17 | "dependencies": 18 | { 19 | "dplug:lv2": "~>13.0", 20 | "dplug:au": "~>13.0", 21 | "dplug:vst2": "~>13.0", 22 | "dplug:vst3": "~>13.0", 23 | "kdr": { "path": "../.." }, 24 | }, 25 | 26 | "configurations": [ 27 | { 28 | "name": "VST3", 29 | "versions": ["VST3"], 30 | "targetType": "dynamicLibrary", 31 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-vst3.lst", "-dead_strip" ], 32 | "lflags-linux-ldc": [ "--version-script=../../resource/module-vst3.ver" ] 33 | }, 34 | { 35 | "name": "VST2", 36 | "versions": ["VST2"], 37 | "targetType": "dynamicLibrary", 38 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-vst.lst", "-dead_strip" ], 39 | "lflags-linux-ldc": [ "--version-script=../../resource/module-vst.ver" ] 40 | }, 41 | { 42 | "name": "AU", 43 | "versions": ["AU"], 44 | "targetType": "dynamicLibrary", 45 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-au.lst", "-dead_strip" ] 46 | }, 47 | { 48 | "name": "LV2", 49 | "versions": ["LV2"], 50 | "targetType": "dynamicLibrary", 51 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-lv2.lst", "-dead_strip" ], 52 | "lflags-linux-ldc": [ "--version-script=../../resource/module-lv2.ver" ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /bin/envtool/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Pure-D/code-d/master/json-validation/dub.schema.json", 3 | 4 | "name": "envtool", 5 | 6 | "license": "BSL-1.0", 7 | "importPaths": [ "." ], 8 | "sourcePaths": [ "." ], 9 | "stringImportPaths": ["../../resource", "."], 10 | 11 | "dflags-linux-dmd": ["-defaultlib=libphobos2.a"], 12 | "dflags-osx-ldc": ["-static"], 13 | "dflags-linux-ldc": ["-link-defaultlib-shared=false"], 14 | "dflags-linux-x86_64-ldc": ["-fvisibility=hidden"], 15 | "dflags-windows-ldc": ["-mscrtlib=libcmt","-fvisibility=hidden", "-link-defaultlib-shared=false"], 16 | 17 | "dependencies": 18 | { 19 | "dplug:lv2": "~>13.0", 20 | "dplug:au": "~>13.0", 21 | "dplug:vst2": "~>13.0", 22 | "dplug:vst3": "~>13.0", 23 | "kdr": { "path": "../.." }, 24 | }, 25 | 26 | "configurations": [ 27 | { 28 | "name": "VST3", 29 | "versions": ["VST3"], 30 | "targetType": "dynamicLibrary", 31 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-vst3.lst", "-dead_strip" ], 32 | "lflags-linux-ldc": [ "--version-script=../../resource/module-vst3.ver" ] 33 | }, 34 | { 35 | "name": "VST2", 36 | "versions": ["VST2"], 37 | "targetType": "dynamicLibrary", 38 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-vst.lst", "-dead_strip" ], 39 | "lflags-linux-ldc": [ "--version-script=../../resource/module-vst.ver" ] 40 | }, 41 | { 42 | "name": "AU", 43 | "versions": ["AU"], 44 | "targetType": "dynamicLibrary", 45 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-au.lst", "-dead_strip" ] 46 | }, 47 | { 48 | "name": "LV2", 49 | "versions": ["LV2"], 50 | "targetType": "dynamicLibrary", 51 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-lv2.lst", "-dead_strip" ], 52 | "lflags-linux-ldc": [ "--version-script=../../resource/module-lv2.ver" ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /bin/epiano2/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Pure-D/code-d/master/json-validation/dub.schema.json", 3 | 4 | "name": "epiano2", 5 | 6 | "license": "BSL-1.0", 7 | "importPaths": [ "." ], 8 | "sourcePaths": [ "." ], 9 | "stringImportPaths": ["../../resource", "."], 10 | 11 | "dflags-linux-dmd": ["-defaultlib=libphobos2.a"], 12 | "dflags-osx-ldc": ["-static"], 13 | "dflags-linux-ldc": ["-link-defaultlib-shared=false"], 14 | "dflags-linux-x86_64-ldc": ["-fvisibility=hidden"], 15 | "dflags-windows-ldc": ["-mscrtlib=libcmt","-fvisibility=hidden", "-link-defaultlib-shared=false"], 16 | 17 | "dependencies": 18 | { 19 | "dplug:lv2": "~>13.0", 20 | "dplug:au": "~>13.0", 21 | "dplug:vst2": "~>13.0", 22 | "dplug:vst3": "~>13.0", 23 | "kdr": { "path": "../.." }, 24 | }, 25 | 26 | "configurations": [ 27 | { 28 | "name": "VST3", 29 | "versions": ["VST3"], 30 | "targetType": "dynamicLibrary", 31 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-vst3.lst", "-dead_strip" ], 32 | "lflags-linux-ldc": [ "--version-script=../../resource/module-vst3.ver" ] 33 | }, 34 | { 35 | "name": "VST2", 36 | "versions": ["VST2"], 37 | "targetType": "dynamicLibrary", 38 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-vst.lst", "-dead_strip" ], 39 | "lflags-linux-ldc": [ "--version-script=../../resource/module-vst.ver" ] 40 | }, 41 | { 42 | "name": "AU", 43 | "versions": ["AU"], 44 | "targetType": "dynamicLibrary", 45 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-au.lst", "-dead_strip" ] 46 | }, 47 | { 48 | "name": "LV2", 49 | "versions": ["LV2"], 50 | "targetType": "dynamicLibrary", 51 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-lv2.lst", "-dead_strip" ], 52 | "lflags-linux-ldc": [ "--version-script=../../resource/module-lv2.ver" ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /bin/freeverb/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Pure-D/code-d/master/json-validation/dub.schema.json", 3 | 4 | "name": "freeverb", 5 | 6 | "license": "BSL-1.0", 7 | "importPaths": [ "." ], 8 | "sourcePaths": [ "." ], 9 | "stringImportPaths": ["../../resource", "."], 10 | 11 | "dflags-linux-dmd": ["-defaultlib=libphobos2.a"], 12 | "dflags-osx-ldc": ["-static"], 13 | "dflags-linux-ldc": ["-link-defaultlib-shared=false"], 14 | "dflags-linux-x86_64-ldc": ["-fvisibility=hidden"], 15 | "dflags-windows-ldc": ["-mscrtlib=libcmt","-fvisibility=hidden", "-link-defaultlib-shared=false"], 16 | 17 | "dependencies": 18 | { 19 | "dplug:lv2": "~>13.0", 20 | "dplug:au": "~>13.0", 21 | "dplug:vst2": "~>13.0", 22 | "dplug:vst3": "~>13.0", 23 | "kdr": { "path": "../.." }, 24 | }, 25 | 26 | "configurations": [ 27 | { 28 | "name": "VST3", 29 | "versions": ["VST3"], 30 | "targetType": "dynamicLibrary", 31 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-vst3.lst", "-dead_strip" ], 32 | "lflags-linux-ldc": [ "--version-script=../../resource/module-vst3.ver" ] 33 | }, 34 | { 35 | "name": "VST2", 36 | "versions": ["VST2"], 37 | "targetType": "dynamicLibrary", 38 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-vst.lst", "-dead_strip" ], 39 | "lflags-linux-ldc": [ "--version-script=../../resource/module-vst.ver" ] 40 | }, 41 | { 42 | "name": "AU", 43 | "versions": ["AU"], 44 | "targetType": "dynamicLibrary", 45 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-au.lst", "-dead_strip" ] 46 | }, 47 | { 48 | "name": "LV2", 49 | "versions": ["LV2"], 50 | "targetType": "dynamicLibrary", 51 | "lflags-osx-ldc": [ "-exported_symbols_list", "../../resource/module-lv2.lst", "-dead_strip" ], 52 | "lflags-linux-ldc": [ "--version-script=../../resource/module-lv2.ver" ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /source/kdr/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 kdr.modfilter; 8 | 9 | import dplug.client.midi; 10 | import mir.math.common : fmin, fmax, fastmath; 11 | 12 | import kdr.envelope; 13 | import kdr.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/kdr/delay.d: -------------------------------------------------------------------------------- 1 | module kdr.delay; 2 | 3 | import kdr.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/kdr/params.d: -------------------------------------------------------------------------------- 1 | /// Parameter utility. 2 | module kdr.params; 3 | 4 | import std.traits : getUDAs, EnumMembers; 5 | 6 | import dplug.core.vec : makeVec, Vec; 7 | import dplug.client.params : Parameter, EnumParameter, IntegerParameter; 8 | 9 | 10 | /// UDA for registering builder struct of param enum. 11 | /// Params: 12 | /// T = Builder struct. 13 | struct RegisterBuilder(T) { 14 | alias Builder = T; 15 | } 16 | 17 | /// For dplug.client.Client.buildParameters. 18 | /// Params: 19 | /// E = Parameter ID enum. 20 | /// Returns: a type-erased parameter slice of the given enum Params. 21 | Parameter[] buildParams(E)() { 22 | Vec!Parameter params = makeVec!Parameter(EnumMembers!E.length); 23 | alias ParamBuilder = getUDAs!(E, RegisterBuilder)[0].Builder; 24 | static foreach (i, pname; __traits(allMembers, E)) { 25 | params[i] = __traits(getMember, ParamBuilder, pname)(); 26 | assert(i == params[i].index, pname ~ " has wrong index."); 27 | } 28 | return params.releaseData(); 29 | } 30 | 31 | 32 | /// Casts types from untyped parameters using parameter id. 33 | /// Params: 34 | /// pid = Params enum id. 35 | /// params = type-erased parameter array. 36 | /// Returns: statically-known typed param. 37 | auto typedParam(alias pid)(Parameter[] params) { 38 | alias Params = typeof(pid); 39 | alias ParamBuilder = getUDAs!(Params, RegisterBuilder)[0].Builder; 40 | alias T = typeof(__traits(getMember, ParamBuilder, __traits(allMembers, Params)[pid])()); 41 | return cast(T) params[pid]; 42 | } 43 | 44 | /// Example parameters. 45 | version (unittest) { 46 | /// Parameter ID. 47 | @RegisterBuilder!TestParamBuilder 48 | enum TestParams { 49 | volume, 50 | wave, 51 | } 52 | 53 | /// Parameter builder corresponding to TestParams fields. 54 | struct TestParamBuilder { 55 | static volume() { 56 | return new IntegerParameter(TestParams.volume, "volume", "%", 0, 100, 50); 57 | } 58 | 59 | static wave() { 60 | return new EnumParameter(TestParams.wave, "wave", ["saw", "sin"], 0); 61 | } 62 | } 63 | } 64 | 65 | /// Example to safely convert typed <-> type-erased parameters. 66 | unittest { 67 | Parameter[] ps = buildParams!TestParams; 68 | // Access parameters via builder definitions. 69 | const IntegerParameter i = typedParam!(TestParams.volume)(ps); 70 | assert(i !is null); 71 | const EnumParameter e = typedParam!(TestParams.wave)(ps); 72 | assert(e !is null); 73 | } 74 | -------------------------------------------------------------------------------- /source/kdr/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 kdr.waveform; 8 | 9 | import mir.math : sin, PI, M_1_PI, M_2_PI, fmin, fastmath, fmuladd; 10 | 11 | import kdr.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 fmuladd(- 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 fmuladd(M_2_PI, fmin(this.phase, 2 * PI - this.phase), -1f); 43 | case Waveform.noise: 44 | return fmuladd(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 | -------------------------------------------------------------------------------- /source/kdr/ringbuffer.d: -------------------------------------------------------------------------------- 1 | module kdr.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 | inout(T)[] slice() inout { return _ptr[0 .. length]; } 62 | 63 | alias slice this; 64 | 65 | private: 66 | T* _ptr; 67 | size_t _capacity, _front_idx, _back_idx; 68 | } 69 | 70 | @nogc nothrow pure 71 | unittest { 72 | RingBuffer!float buf; 73 | buf.recalloc(2); 74 | buf.resize(2); 75 | assert(buf.length == 2); 76 | assert(buf.front == 0); 77 | 78 | buf.enqueue(1); 79 | assert(buf.front == 0); 80 | assert(buf.length == 2); 81 | 82 | buf.enqueue(2); 83 | assert(buf.front == 1); 84 | assert(buf.length == 2); 85 | 86 | // resize shorter 87 | buf.resize(1); 88 | assert(buf.length == 1); 89 | assert(buf.front == 2); 90 | 91 | // resize longer 92 | buf.resize(2); 93 | assert(buf.length == 2); 94 | assert(buf.front == 1); // previous front 95 | } 96 | -------------------------------------------------------------------------------- /resource/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 resource/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 | -------------------------------------------------------------------------------- /rules/BUILD: -------------------------------------------------------------------------------- 1 | """Bazel rules. 2 | 3 | Flags: 4 | 5 | To switch D compiler, set: --//rules:d_compiler=ldc2 (default: dmd) 6 | """ 7 | package(default_visibility = ["//visibility:public"]) 8 | load(":d_toolchain.bzl", "d_compiler") 9 | 10 | d_compiler(name = "d_compiler", build_setting_default = "dmd") 11 | 12 | filegroup( 13 | name = "srcs", 14 | srcs = glob(["**"]), 15 | ) 16 | 17 | config_setting( 18 | name = "darwin", 19 | values = {"host_cpu": "darwin"}, 20 | ) 21 | 22 | config_setting( 23 | name = "k8", 24 | values = {"host_cpu": "k8"}, 25 | ) 26 | 27 | config_setting( 28 | name = "x64_windows", 29 | values = {"host_cpu": "x64_windows"}, 30 | ) 31 | 32 | filegroup( 33 | name = "dmd", 34 | srcs = select({ 35 | ":darwin": ["@dmd_darwin_x86_64//:dmd"], 36 | ":k8": ["@dmd_linux_x86_64//:dmd"], 37 | ":x64_windows": ["@dmd_windows_x86_64//:dmd"], 38 | }), 39 | ) 40 | 41 | filegroup( 42 | name = "libphobos2", 43 | srcs = select({ 44 | ":darwin": ["@dmd_darwin_x86_64//:libphobos2"], 45 | ":k8": ["@dmd_linux_x86_64//:libphobos2"], 46 | ":x64_windows": ["@dmd_windows_x86_64//:libphobos2"], 47 | }), 48 | ) 49 | 50 | filegroup( 51 | name = "phobos-src", 52 | srcs = select({ 53 | ":darwin": ["@dmd_darwin_x86_64//:phobos-src"], 54 | ":k8": ["@dmd_linux_x86_64//:phobos-src"], 55 | ":x64_windows": ["@dmd_windows_x86_64//:phobos-src"], 56 | }), 57 | ) 58 | 59 | filegroup( 60 | name = "druntime-import-src", 61 | srcs = select({ 62 | ":darwin": ["@dmd_darwin_x86_64//:druntime-import-src"], 63 | ":k8": ["@dmd_linux_x86_64//:druntime-import-src"], 64 | ":x64_windows": ["@dmd_windows_x86_64//:druntime-import-src"], 65 | }), 66 | ) 67 | 68 | filegroup( 69 | name = "ldc2", 70 | srcs = select({ 71 | ":darwin": ["@ldc2_darwin_x86_64//:ldc2"], 72 | ":k8": ["@ldc2_linux_x86_64//:ldc2"], 73 | ":x64_windows": ["@ldc2_windows_x86_64//:ldc2"], 74 | }), 75 | ) 76 | 77 | filegroup( 78 | name = "libphobos2-ldc2", 79 | srcs = select({ 80 | ":darwin": ["@ldc2_darwin_x86_64//:libphobos2"], 81 | ":k8": ["@ldc2_linux_x86_64//:libphobos2"], 82 | ":x64_windows": ["@ldc2_windows_x86_64//:libphobos2"], 83 | }), 84 | ) 85 | 86 | filegroup( 87 | name = "phobos-src-ldc2", 88 | srcs = select({ 89 | ":darwin": ["@ldc2_darwin_x86_64//:phobos-src"], 90 | ":k8": ["@ldc2_linux_x86_64//:phobos-src"], 91 | ":x64_windows": ["@ldc2_windows_x86_64//:phobos-src"], 92 | }), 93 | ) 94 | 95 | filegroup( 96 | name = "druntime-import-src-ldc2", 97 | srcs = select({ 98 | ":darwin": ["@ldc2_darwin_x86_64//:druntime-import-src"], 99 | ":k8": ["@ldc2_linux_x86_64//:druntime-import-src"], 100 | ":x64_windows": ["@ldc2_windows_x86_64//:druntime-import-src"], 101 | }), 102 | ) 103 | -------------------------------------------------------------------------------- /rules/dmd.bzl: -------------------------------------------------------------------------------- 1 | """DMD rules.""" 2 | 3 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 4 | 5 | DMD_BUILD_FILE = """ 6 | package(default_visibility = ["//visibility:public"]) 7 | 8 | config_setting( 9 | name = "darwin", 10 | values = {"host_cpu": "darwin"}, 11 | ) 12 | 13 | config_setting( 14 | name = "k8", 15 | values = {"host_cpu": "k8"}, 16 | ) 17 | 18 | config_setting( 19 | name = "x64_windows", 20 | values = {"host_cpu": "x64_windows"}, 21 | ) 22 | 23 | filegroup( 24 | name = "dmd", 25 | srcs = select({ 26 | ":darwin": ["dmd2/osx/bin/dmd"], 27 | ":k8": ["dmd2/linux/bin64/dmd"], 28 | ":x64_windows": ["dmd2/windows/bin64/dmd.exe"], 29 | }), 30 | ) 31 | 32 | filegroup( 33 | name = "libphobos2", 34 | srcs = select({ 35 | ":darwin": ["dmd2/osx/lib/libphobos2.a"], 36 | ":k8": [ 37 | "dmd2/linux/lib64/libphobos2.a", 38 | "dmd2/linux/lib64/libphobos2.so", 39 | ], 40 | ":x64_windows": ["dmd2/windows/lib64/phobos64.lib"], 41 | }), 42 | ) 43 | 44 | filegroup( 45 | name = "phobos-src", 46 | srcs = glob(["dmd2/src/phobos/**/*.*"]), 47 | ) 48 | 49 | filegroup( 50 | name = "druntime-import-src", 51 | srcs = glob([ 52 | "dmd2/src/druntime/import/*.*", 53 | "dmd2/src/druntime/import/**/*.*", 54 | ]), 55 | ) 56 | """ 57 | 58 | def dmd_repositories(): 59 | http_archive( 60 | name = "dmd_linux_x86_64", 61 | urls = [ 62 | "http://downloads.dlang.org/releases/2021/dmd.2.097.1.linux.tar.xz", 63 | ], 64 | sha256 = "030fd1bc3b7308dadcf08edc1529d4a2e46496d97ee92ed532b246a0f55745e6", 65 | build_file_content = DMD_BUILD_FILE, 66 | ) 67 | 68 | http_archive( 69 | name = "dmd_darwin_x86_64", 70 | urls = [ 71 | "http://downloads.dlang.org/releases/2021/dmd.2.097.1.osx.tar.xz", 72 | ], 73 | sha256 = "383a5524266417bcdd3126da947be7caebd4730f789021e9ec26d869c8448f6a", 74 | build_file_content = DMD_BUILD_FILE, 75 | ) 76 | 77 | http_archive( 78 | name = "dmd_windows_x86_64", 79 | urls = [ 80 | "http://downloads.dlang.org/releases/2021/dmd.2.097.1.windows.zip", 81 | ], 82 | sha256 = "63a00e624bf23ab676c543890a93b5325d6ef6b336dee2a2f739f2bbcef7ef1f", 83 | build_file_content = DMD_BUILD_FILE, 84 | ) 85 | 86 | dmd_compile_attrs = { 87 | "_dmd_flag_version": attr.string(default = "-version"), 88 | "_dmd_compiler": attr.label( 89 | default = Label("//rules:dmd"), 90 | executable = True, 91 | allow_single_file = True, 92 | cfg = "host", 93 | ), 94 | "_dmd_runtime_import_src": attr.label( 95 | default = Label("//rules:druntime-import-src"), 96 | ), 97 | "_dmd_stdlib": attr.label( 98 | default = Label("//rules:libphobos2"), 99 | ), 100 | "_dmd_stdlib_src": attr.label( 101 | default = Label("//rules:phobos-src"), 102 | ), 103 | } 104 | -------------------------------------------------------------------------------- /source/kdr/chorus.d: -------------------------------------------------------------------------------- 1 | module kdr.chorus; 2 | 3 | import dplug.client.client : TimeInfo; 4 | 5 | import kdr.delay : Delay, DelayKind; 6 | import kdr.lfo : LFO, Multiplier; 7 | import kdr.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/kdr/BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | load("//rules:d.bzl", "d_library", "d_test", "d_library_with_test") 4 | 5 | d_library_with_test( 6 | name = "filter", 7 | srcs = ["filter.d"], 8 | string_imports = ["//resource:filter_coeff"], 9 | imports = [".."], 10 | deps = [ 11 | "@mir-core//:math" 12 | ], 13 | ) 14 | 15 | d_library_with_test( 16 | name = "random", 17 | srcs = ["random.d"], 18 | imports = [".."], 19 | ) 20 | 21 | d_library_with_test( 22 | name = "waveform", 23 | srcs = ["waveform.d"], 24 | imports = [".."], 25 | deps = [ 26 | ":random", 27 | "@mir-core//:math" 28 | ], 29 | ) 30 | 31 | d_library_with_test( 32 | name = "ringbuffer", 33 | srcs = ["ringbuffer.d"], 34 | imports = [".."], 35 | ) 36 | 37 | d_library_with_test( 38 | name = "delay", 39 | srcs = ["delay.d"], 40 | imports = [".."], 41 | deps = [":ringbuffer"], 42 | ) 43 | 44 | d_library_with_test( 45 | name = "effect", 46 | srcs = ["effect.d"], 47 | imports = [".."], 48 | deps = [ 49 | ":filter", 50 | ":waveform", 51 | "@Dplug//:core", 52 | "@mir-core//:math", 53 | ], 54 | ) 55 | 56 | d_library_with_test( 57 | name = "lfo", 58 | srcs = ["lfo.d"], 59 | imports = [".."], 60 | deps = [ 61 | ":waveform", 62 | "@Dplug//:core", 63 | "@Dplug//:client", 64 | ], 65 | ) 66 | 67 | d_library_with_test( 68 | name = "envelope", 69 | srcs = ["envelope.d"], 70 | imports = [".."], 71 | deps = [ 72 | "@Dplug//:client", 73 | "@Dplug//:core", 74 | "@Dplug//:math", 75 | "@mir-core//:math", 76 | ], 77 | ) 78 | 79 | d_library_with_test( 80 | name = "equalizer", 81 | srcs = ["equalizer.d"], 82 | imports = [".."], 83 | deps = [ 84 | ":filter", 85 | "@mir-core//:math", 86 | ], 87 | ) 88 | 89 | d_library_with_test( 90 | name = "modfilter", 91 | srcs = ["modfilter.d"], 92 | imports = [".."], 93 | deps = [ 94 | ":envelope", 95 | ":filter", 96 | "@Dplug//:client", 97 | "@mir-core//:math", 98 | ], 99 | ) 100 | 101 | d_library_with_test( 102 | name = "voice", 103 | srcs = ["voice.d"], 104 | imports = [".."], 105 | deps = [ 106 | ":envelope", 107 | "@mir-core//:math", 108 | ], 109 | ) 110 | 111 | d_library_with_test( 112 | name = "oscillator", 113 | srcs = ["oscillator.d"], 114 | imports = [".."], 115 | deps = [ 116 | ":waveform", 117 | ":voice", 118 | "@Dplug//:client", 119 | "@mir-core//:math", 120 | ], 121 | ) 122 | 123 | d_library_with_test( 124 | name = "params", 125 | srcs = ["params.d"], 126 | imports = [".."], 127 | deps = [ 128 | "@Dplug//:client", 129 | "@Dplug//:core", 130 | ], 131 | ) 132 | 133 | d_library_with_test( 134 | name = "logging", 135 | srcs = ["logging.d"], 136 | imports = [".."], 137 | deps = [ 138 | "@Dplug//:core", 139 | ], 140 | ) 141 | -------------------------------------------------------------------------------- /.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 | 14 | defaults: 15 | run: 16 | shell: pwsh 17 | 18 | jobs: 19 | Test: 20 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 21 | 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | os: 27 | - windows-latest 28 | - ubuntu-latest 29 | - macOS-latest 30 | plugin: 31 | - synth2 32 | - freeverb 33 | - envtool 34 | - epiano2 35 | compiler: 36 | - 'ldc-latest' 37 | steps: 38 | # Checkout 39 | - name: Checkout master branch 40 | uses: actions/checkout@v3 41 | with: 42 | fetch-depth: 0 43 | 44 | # Cache 45 | - name: Cache 46 | id: kdr-cache 47 | uses: actions/cache@v3 48 | with: 49 | path: ${{ env.VST2_SDK }} 50 | key: kdr-cache 51 | 52 | # Install 53 | - name: Install Dependencies - Ubuntu 54 | if: startsWith(matrix.os,'ubuntu') 55 | run: | 56 | sudo apt-get -yq install libx11-dev 57 | 58 | - name: Install compiler 59 | uses: dlang-community/setup-dlang@v1 60 | with: 61 | compiler: ${{ matrix.compiler }} 62 | 63 | - name: Setup VST2_SDK 64 | if: contains(env.SETUP_VST2_SDK, 'true') && steps.kdr-cache.outputs.cache-hit != 'true' 65 | run: | 66 | curl -LOJ https://web.archive.org/web/20200502121517if_/https://www.steinberg.net/sdk_downloads/vstsdk366_27_06_2016_build_61.zip 67 | 7z x ./vstsdk366_27_06_2016_build_61.zip 68 | mkdir -p ${{ env.VST2_SDK }}/pluginterfaces/vst2.x 69 | cp "./VST3 SDK/pluginterfaces/vst2.x/aeffect.h" ${{ env.VST2_SDK }}/pluginterfaces/vst2.x/aeffect.h 70 | cp "./VST3 SDK/pluginterfaces/vst2.x/aeffectx.h" ${{ env.VST2_SDK }}/pluginterfaces/vst2.x/aeffectx.h 71 | 72 | - name: Build 73 | run: | 74 | if ("${{ matrix.os }}" -like 'windows*') { 75 | $Plugins = "-c VST2 -c VST3" 76 | } elseif ("${{ matrix.os }}" -like 'macOS*') { 77 | $Plugins = "-c VST2 -c VST3 -c AU -a x86_64" 78 | } elseif ("${{ matrix.os }}" -like 'ubuntu*') { 79 | $Plugins = "-c VST2 -c VST3 -c LV2" 80 | } 81 | $esc = '--%' 82 | dub run dplug:dplug-build -- $esc $Plugins --final 83 | working-directory: bin/${{ matrix.plugin }} 84 | 85 | - name: Archive zip 86 | run: | 87 | 7z a ${{ matrix.os }}-${{ matrix.plugin }}.zip ./bin/${{ matrix.plugin }}/builds 88 | 89 | - name: Upload 90 | uses: actions/upload-artifact@v3 91 | with: 92 | name: ${{ matrix.os }}-${{ matrix.plugin }} 93 | path: ${{ matrix.os }}-${{ matrix.plugin }}.zip 94 | 95 | - name: Release 96 | uses: softprops/action-gh-release@v1 97 | if: startsWith(github.ref, 'refs/tags/') 98 | with: 99 | files: ${{ matrix.os }}-${{ matrix.plugin }}.zip 100 | 101 | Skip: 102 | if: "contains(github.event.head_commit.message, '[skip ci]')" 103 | runs-on: ubuntu-latest 104 | steps: 105 | - name: Skip CI 🚫 106 | run: echo skip CI 107 | -------------------------------------------------------------------------------- /source/kdr/epiano2/parameter.d: -------------------------------------------------------------------------------- 1 | module kdr.epiano2.parameter; 2 | 3 | import core.stdc.stdio : snprintf, sscanf; 4 | import std.algorithm : clamp; 5 | 6 | import dplug.client.params : IntegerParameter; 7 | 8 | /// Uses a negative value for "Pan" and positive for "Trem". 9 | class ModParameter : IntegerParameter { 10 | nothrow @nogc: 11 | 12 | this(int index, string name, string label, 13 | int min = 0, int max = 1, int defaultValue = 0) { 14 | super(index, name, label, min, max, defaultValue); 15 | this._min = min; 16 | this._max = max; 17 | } 18 | 19 | override void toStringN(char* buffer, size_t numBytes) { 20 | printValue(buffer, numBytes, value()); 21 | } 22 | 23 | override void stringFromNormalizedValue( 24 | double normalizedValue, char* buffer, size_t len) const { 25 | printValue(buffer, len, fromNormalized(normalizedValue)); 26 | } 27 | 28 | override bool normalizedValueFromString( 29 | const(char)[] valueString, out double result) const { 30 | if (valueString.length > 63) 31 | return false; 32 | 33 | // Because the input string is not zero-terminated 34 | char[64] buf; 35 | snprintf(buf.ptr, buf.length, "%.*s", cast(int)(valueString.length), 36 | valueString.ptr); 37 | 38 | int denorm; 39 | if (buf[0 .. 5] == "Trem " && 1 == sscanf(buf.ptr + 5, "%d", &denorm)) { 40 | result = toNormalized(denorm); 41 | return true; 42 | } 43 | if (buf[0 .. 4] == "Pan " && 1 == sscanf(buf.ptr + 4, "%d", &denorm)) { 44 | result = toNormalized(-denorm); 45 | return true; 46 | } 47 | return false; 48 | } 49 | 50 | private: 51 | // Funcs from base class because they are private. 52 | int fromNormalized(double normalizedValue) const { 53 | double mapped = _min + (_max - _min) * normalizedValue; 54 | 55 | // slightly incorrect rounding, but lround is crashing 56 | int rounded = void; 57 | if (mapped >= 0) 58 | rounded = cast(int)(0.5f + mapped); 59 | else 60 | rounded = cast(int)(-0.5f + mapped); 61 | 62 | return clamp(rounded, _min, _max); 63 | } 64 | 65 | double toNormalized(int value) const { 66 | return clamp( (cast(double)value - _min) / (_max - _min), 0.0, 1.0); 67 | } 68 | 69 | int _min; 70 | int _max; 71 | } 72 | 73 | private void printValue(char* buffer, size_t numBytes, int v) @nogc nothrow { 74 | if (v > 0) { 75 | snprintf(buffer, numBytes, "Trem %d", v); 76 | return; 77 | } 78 | snprintf(buffer, numBytes, "Pan %d", -v); 79 | } 80 | 81 | unittest { 82 | import std.string : fromStringz; 83 | 84 | auto p = new ModParameter(0, "mod", "", -100, 100, 0); 85 | char[100] buf; 86 | p.toStringN(buf.ptr, buf.length); 87 | import std; 88 | assert(buf.ptr.fromStringz == "Pan 0"); 89 | p.setFromHost(1); 90 | p.toStringN(buf.ptr, buf.length); 91 | assert(buf.ptr.fromStringz == "Trem 100"); 92 | 93 | double result; 94 | assert(p.normalizedValueFromString("Trem 42", result)); 95 | p.stringFromNormalizedValue(result, buf.ptr, buf.length); 96 | assert(buf.ptr.fromStringz == "Trem 42"); 97 | assert(p.fromNormalized(result) == 42); 98 | 99 | assert(p.normalizedValueFromString("Pan 42", result)); 100 | p.stringFromNormalizedValue(result, buf.ptr, buf.length); 101 | assert(buf.ptr.fromStringz == "Pan 42"); 102 | assert(p.fromNormalized(result) == -42); 103 | 104 | assert(!p.normalizedValueFromString(buf, result), 105 | "Should fail because of too long str."); 106 | assert(!p.normalizedValueFromString("nonsense str", result)); 107 | } 108 | -------------------------------------------------------------------------------- /source/kdr/random.d: -------------------------------------------------------------------------------- 1 | module kdr.random; 2 | 3 | @nogc nothrow @safe pure 4 | private uint rotl(const uint x, int k) { 5 | return (x << k) | (x >> (32 - k)); 6 | } 7 | 8 | @nogc nothrow @safe pure 9 | private 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 | /// Random number generator based on https://prng.di.unimi.it/xoshiro128plus.c 17 | struct Xorshiro128Plus { 18 | @nogc nothrow pure @safe: 19 | 20 | /// ctor. 21 | this(ulong seed) { 22 | s[0] = cast(uint) seed; 23 | s[1] = seed >> 32; 24 | 25 | const ulong sp = splitmix64(seed); 26 | s[2] = cast(uint) sp; 27 | s[3] = sp >> 32; 28 | 29 | assert(!(s[0] == 0 && s[1] == 0 && s[2] == 0 && s[3] == 0)); 30 | } 31 | 32 | /// Returns: a random number. 33 | uint front() const { 34 | return s[0] + s[3]; 35 | } 36 | 37 | /// Updates random states. 38 | void popFront() { 39 | const uint t = s[1] << 9; 40 | 41 | s[2] ^= s[0]; 42 | s[3] ^= s[1]; 43 | s[1] ^= s[2]; 44 | s[0] ^= s[3]; 45 | 46 | s[2] ^= t; 47 | 48 | s[3] = rotl(s[3], 11); 49 | } 50 | 51 | /// This is the jump function for the generator. It is equivalent 52 | /// to 2^64 calls to popFront(); it can be used to generate 2^64 53 | /// non-overlapping subsequences for parallel computations. 54 | void jump() { 55 | _jump!([0x8764000b, 0xf542d2d3, 0x6fa035c3, 0x77f2db5b]); 56 | } 57 | 58 | /// This is the long-jump function for the generator. It is equivalent to 59 | /// 2^96 calls to popFront(); it can be used to generate 2^32 starting points, 60 | /// from each of which jump() will generate 2^32 non-overlapping 61 | /// subsequences for parallel distributed computations. 62 | void longJump() { 63 | _jump!([0xb523952e, 0x0b6f099f, 0xccf5a0ef, 0x1c580662]); 64 | } 65 | 66 | private: 67 | 68 | void _jump(const uint[4] JUMP)() { 69 | uint[4] a; 70 | foreach (j; JUMP) { 71 | foreach (b; 0 .. 32) { 72 | if (j & 1u << b) { 73 | a[] ^= s[]; 74 | } 75 | popFront(); 76 | } 77 | } 78 | s[] = a[]; 79 | } 80 | 81 | /// Random number states. 82 | uint[4] s = [0, 0, cast(uint) splitmix64(0), splitmix64(0) >> 32]; 83 | } 84 | 85 | @nogc nothrow pure @safe 86 | unittest { 87 | Xorshiro128Plus rng0, rng1, rng2; 88 | assert(rng0.s == rng1.s, "initial seeds should be equal."); 89 | 90 | const uint x0 = rng0.front(); 91 | assert(x0 == rng1.front(), "front should be the same if seeds are equal."); 92 | 93 | rng0.popFront(); 94 | assert(rng0.front != x0, "front should be changed by popFront."); 95 | 96 | rng1.popFront(); 97 | assert(rng0.front == rng1.front(), "popFront() should be reproducible"); 98 | 99 | rng1.jump(); 100 | assert(rng0.front != rng1.front, "jump() should mutate front."); 101 | 102 | rng0.jump(); 103 | assert(rng0.front == rng1.front, 104 | "Both rng0 and rng1 call 1 jump + 1 popFront in total."); 105 | 106 | rng2.popFront(); 107 | rng2.jump(); 108 | rng2.longJump(); 109 | assert(rng0.front != rng2.front, "longJump() should mutate front."); 110 | 111 | rng0.longJump(); 112 | assert(rng0.front == rng2.front, 113 | "Both rng0 and rng2 call 1 longJump + 1 jump + 1 popFront in total."); 114 | 115 | 116 | assert(Xorshiro128Plus.init.s == Xorshiro128Plus(0).s, 117 | "Zero seeded states should be equal to the default-initialized ones."); 118 | 119 | assert(Xorshiro128Plus.init.s != Xorshiro128Plus(1).s, 120 | "Non-zero states should be different from the default-initialized ones."); 121 | } 122 | -------------------------------------------------------------------------------- /source/kdr/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 kdr.voice; 8 | 9 | import mir.math.common : fastmath; 10 | 11 | import kdr.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/kdr/envtool/client.d: -------------------------------------------------------------------------------- 1 | module kdr.envtool.client; 2 | 3 | import std.algorithm.comparison : clamp; 4 | 5 | import dplug.math : vec2f; 6 | import dplug.client : Client, IGraphics, LegalIO, LinearFloatParameter, Parameter, PluginInfo, TimeInfo; 7 | import dplug.core; 8 | 9 | import kdr.envelope : Envelope; 10 | import kdr.envtool.gui : EnvToolGUI; 11 | import kdr.envtool.params; 12 | import kdr.filter; 13 | import kdr.logging : logInfo; 14 | import kdr.testing : benchmarkWithDefaultParams; 15 | 16 | /// Env tool client. 17 | class EnvToolClient : Client { 18 | public nothrow @nogc: 19 | 20 | /// Ctor. 21 | this() { 22 | super(); 23 | logInfo("Initialize %s", __FUNCTION__.ptr); 24 | } 25 | 26 | override IGraphics createGraphics() { 27 | if (!_gui) _gui = mallocNew!EnvToolGUI(params); 28 | return _gui; 29 | } 30 | 31 | override Parameter[] buildParameters() { 32 | return buildEnvelopeParameters(); 33 | } 34 | 35 | @safe 36 | override PluginInfo buildPluginInfo() { 37 | return PluginInfo.init; 38 | } 39 | 40 | override LegalIO[] buildLegalIO() { 41 | Vec!LegalIO io = makeVec!LegalIO(); 42 | io ~= LegalIO(1, 1); 43 | io ~= LegalIO(2, 2); 44 | return io.releaseData(); 45 | } 46 | 47 | @safe 48 | override int maxFramesInProcess() { 49 | return 32; 50 | } 51 | 52 | @safe 53 | override void reset( 54 | double sampleRate, int maxFrames, int numInputs, int numOutputs) { 55 | _sampleRate = sampleRate; 56 | foreach (ref Filter f; _filter) f.setSampleRate(sampleRate); 57 | } 58 | 59 | override void processAudio( 60 | const(float*)[] inputs, float*[] outputs, int frames, TimeInfo info) { 61 | const Destination dst = readParam!Destination(Params.destination); 62 | 63 | // Setup rate. 64 | const Envelope env = buildEnvelope(params); 65 | const double beatScale = rateValues[readParam!int(Params.rate)] * 4; 66 | const float depth = readParam!float(Params.depth); 67 | const double beatPerSample = info.tempo / 60 / _sampleRate; 68 | 69 | // Setup filter. 70 | const fkind = readParam!FilterKind(Params.filterKind); 71 | const fcutoff = readParam!float(Params.filterCutoff); 72 | const fres = readParam!float(Params.filterRes); 73 | foreach (ref f; _filter) f.setParams(fkind, fcutoff, fres); 74 | 75 | foreach (c; 0 .. inputs.length) { 76 | float offset = c == 0 ? 0 : readParam!float(Params.stereoOffset); 77 | foreach (t; 0 .. frames) { 78 | float output = inputs[c][t]; 79 | 80 | if (info.hostIsPlaying) { 81 | // Do envelope modutation. 82 | const double beats = (info.timeInSamples + t) * beatPerSample; 83 | const float e = env.getY((beats / beatScale + offset) % 1.0); 84 | 85 | final switch (dst) { 86 | case Destination.volume: 87 | output *= e; 88 | break; 89 | case Destination.cutoff: 90 | _filter[c].setParams(fkind, e * fcutoff, fres); 91 | break; 92 | case Destination.pan: 93 | float pan = c == 0 ? e : 1.0 - e; 94 | output *= pan; 95 | break; 96 | } 97 | } 98 | 99 | output = _filter[c].apply(output); 100 | // mix dry and wet signals. 101 | outputs[c][t] = depth * output + (1.0 - depth) * inputs[c][t]; 102 | } 103 | } 104 | } 105 | 106 | private: 107 | EnvToolGUI _gui; 108 | double _sampleRate; 109 | Filter[2] _filter; 110 | } 111 | 112 | unittest { 113 | benchmarkWithDefaultParams!EnvToolClient; 114 | } 115 | 116 | // When host is not playing and filter is none. 117 | nothrow unittest { 118 | auto client = new EnvToolClient; 119 | TimeInfo info = {tempo: 120, timeInSamples: -1, hostIsPlaying: false}; 120 | float[] inputs = [1, 2, 3, 4]; 121 | float[][] outputs = new float[][](2, inputs.length); 122 | client.processAudio([&inputs[0], &inputs[0]], [&outputs[0][0], &outputs[1][0]], 123 | cast(int) inputs.length, info); 124 | // Identity outputs. 125 | assert(outputs[0] == inputs); 126 | assert(outputs[1] == inputs); 127 | } 128 | -------------------------------------------------------------------------------- /source/kdr/logging.d: -------------------------------------------------------------------------------- 1 | // -*- mode: d; c-basic-offset: 2 -*- 2 | module kdr.logging; 3 | 4 | import core.thread.types : ThreadID; 5 | import core.stdc.stdio : fprintf, fputc, stderr; 6 | import core.stdc.time : tm; 7 | import std.datetime.systime : Clock, SysTime; 8 | 9 | import dplug.core.nogc : assumeNothrowNoGC; 10 | import dplug.core.sync : UncheckedMutex; 11 | 12 | private ThreadID _thisThreadID() @trusted @nogc nothrow { 13 | version (Windows) { 14 | import core.sys.windows.winbase; 15 | return GetCurrentThreadId(); 16 | } 17 | version (Posix) { 18 | import core.sys.posix.pthread : pthread_self; 19 | return pthread_self(); 20 | } 21 | } 22 | 23 | private __gshared UncheckedMutex outMutex; 24 | 25 | /// Logging time info with usecs. 26 | struct LogTime { 27 | /// Time info except usecs. 28 | tm t; 29 | /// Micro seconds. 30 | long usec; 31 | 32 | alias t this; 33 | } 34 | 35 | private LogTime currentTime() { 36 | // TODO(klknn): Make this @nogc and nothrow. 37 | const SysTime st = Clock.currTime; 38 | return LogTime( 39 | st.toTM(), 40 | st.fracSecs().total!"usecs" % 1_000_000); 41 | } 42 | 43 | private nothrow @nogc 44 | void logImpl(char severity, int line, string f, Args ...)(const(char)* fmt, Args args) { 45 | LogTime t = assumeNothrowNoGC(¤tTime)(); 46 | 47 | // TODO(klknn): Support any buffer outputs in addition to stderr. 48 | outMutex.lockLazy(); 49 | scope (exit) outMutex.unlock(); 50 | 51 | // Based on abseil-py format. 52 | // https://github.com/abseil/abseil-py/blob/9954557f9df0b346a57ff82688438c55202d2188/absl/logging/__init__.py#L731 53 | fprintf( 54 | stderr, 55 | "%c%02d%02d %02d:%02d:%02d.%06ld %5lu %s:%d] ", 56 | severity, 57 | t.tm_mon + 1, t.tm_mday, 58 | t.tm_hour, 59 | t.tm_min, 60 | t.tm_sec, 61 | t.usec, // TODO(klknn): time.millitm, 62 | _thisThreadID, 63 | f.ptr, 64 | line); 65 | fprintf(stderr, fmt, args); 66 | fputc('\n', stderr); 67 | } 68 | 69 | /// Emits log at debug level. 70 | /// Params: 71 | /// fmt = C-style format string. 72 | /// args = arguments to be formatted. 73 | /// line = line number where this log is created. 74 | /// file = file name where this log is created. 75 | nothrow @nogc 76 | void logDebug(int line = __LINE__, string file = __FILE__, Args ...)(const(char)* fmt, Args args) { 77 | debug logImpl!('D', line, file)(fmt, args); 78 | } 79 | 80 | /// Emits log at info level. 81 | /// Params: 82 | /// fmt = C-style format string. 83 | /// args = arguments to be formatted. 84 | /// line = line number where this log is created. 85 | /// file = file name where this log is created. 86 | nothrow @nogc 87 | void logInfo(int line = __LINE__, string file = __FILE__, Args ...)(const(char)* fmt, Args args) { 88 | logImpl!('I', line, file)(fmt, args); 89 | } 90 | 91 | /// Emits log at warning level. 92 | /// Params: 93 | /// fmt = C-style format string. 94 | /// args = arguments to be formatted. 95 | /// line = line number where this log is created. 96 | /// file = file name where this log is created. 97 | nothrow @nogc 98 | void logWarn(int line = __LINE__, string file = __FILE__, Args ...)(const(char)* fmt, Args args) { 99 | logImpl!('W', line, file)(fmt, args); 100 | } 101 | 102 | /// Emits log at error level. 103 | /// Params: 104 | /// fmt = C-style format string. 105 | /// args = arguments to be formatted. 106 | /// line = line number where this log is created. 107 | /// file = file name where this log is created. 108 | nothrow @nogc 109 | void logError(int line = __LINE__, string file = __FILE__, Args ...)(const(char)* fmt, Args args) { 110 | logImpl!('E', line, file)(fmt, args); 111 | assert(false); 112 | } 113 | 114 | unittest { 115 | import core.thread; 116 | import core.time; 117 | 118 | auto other = new Thread({ 119 | foreach (i; 0 .. 2) { 120 | logInfo("%d-th log from other thread %lu.", i, _thisThreadID); 121 | Thread.sleep(dur!"msecs"(10)); 122 | } 123 | }).start(); 124 | foreach (i; 0 .. 2) { 125 | logInfo("%d-th log from this thread %lu.", i, _thisThreadID); 126 | Thread.sleep(dur!"msecs"(10)); 127 | } 128 | other.join(); 129 | } 130 | -------------------------------------------------------------------------------- /rules/ldc2.bzl: -------------------------------------------------------------------------------- 1 | """LDC2 rules for Bazel.""" 2 | 3 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 4 | 5 | def ldc2_sha256dict(s): 6 | d = {} 7 | for line in s.strip().splitlines(): 8 | v, k = line.split(" ") 9 | d[k] = v 10 | return d 11 | 12 | # https://github.com/ldc-developers/ldc/releases/download/v1.28.0/ldc2-1.28.0.sha256sums.txt 13 | _LDC2_SHA256SUMS = ldc2_sha256dict(""" 14 | 17fee8bb535bcb8cda0a45947526555c46c045f302a7349cc8711b254e54cf09 ldc-1.28.0-src.tar.gz 15 | f59936c1c816698ab790b13b4a8cd0b2954bc5f43a38e4dd7ffaa29e28c2f3a6 ldc-1.28.0-src.zip 16 | 52666ebeaeddee402c022cbcdc39b8c27045e6faab15e53c72564b9eaf32ccff ldc2-1.28.0-android-aarch64.tar.xz 17 | c9b22ea84ed5738afcdf1740f501eea68a3269bda7e1a9eb1f139f9c4e5b96de ldc2-1.28.0-android-armv7a.tar.xz 18 | 9786c36c4dfd29dd308a50c499c115e4c2079baeaded07e5ac5396c4a7fd0278 ldc2-1.28.0-linux-x86_64.tar.xz 19 | f9786b8c28d8af1fdd331d8eb889add80285dbebfb97ea47d5dd9110a7df074b ldc2-1.28.0-osx-arm64.tar.xz 20 | 02472507de988c8b5dd83b189c6df3b474741546589496c2ff3d673f26b8d09a ldc2-1.28.0-osx-x86_64.tar.xz 21 | 8917876e2dbe763feec2d2d2ba81f20bfd32ed13753e5ea1bc5ce0ea564f3eaf ldc2-1.28.0-windows-multilib.7z 22 | e6ce44b6533fc4b7639b6ed078bdb107294fefc7b638141c42bf37b46e491990 ldc2-1.28.0-windows-multilib.exe 23 | 26bb3ece7774ef70d9c7485eab5fbc182d4e74411e4a8d2f339e9b421a76f069 ldc2-1.28.0-windows-x64.7z 24 | af5465b316dfb582ded4fd6f83dfa02dfdd896fad6d397cc53d098e3ba9f9281 ldc2-1.28.0-windows-x86.7z 25 | """) 26 | 27 | _LDC2_BUILD_FILE = """ 28 | package(default_visibility = ["//visibility:public"]) 29 | 30 | config_setting( 31 | name = "darwin", 32 | values = {"host_cpu": "darwin"}, 33 | ) 34 | 35 | config_setting( 36 | name = "k8", 37 | values = {"host_cpu": "k8"}, 38 | ) 39 | 40 | config_setting( 41 | name = "x64_windows", 42 | values = {"host_cpu": "x64_windows"}, 43 | ) 44 | 45 | filegroup( 46 | name = "ldc2", 47 | srcs = ["bin/ldc2"], 48 | ) 49 | 50 | filegroup( 51 | name = "libphobos2", 52 | srcs = select({ 53 | ":darwin": ["lib/libphobos2-ldc.a", "lib/libphobos2-ldc-shared.dylib"], 54 | ":k8": ["lib/libphobos2-ldc.a", "lib/libphobos2-ldc-shared.so"], 55 | ":x64_windows": ["lib/phobos2-ldc.lib"], 56 | }), 57 | ) 58 | 59 | filegroup( 60 | name = "phobos-src", 61 | srcs = glob([ 62 | "import/std/*.*", 63 | "import/std/**/*.*", 64 | ]), 65 | ) 66 | 67 | filegroup( 68 | name = "druntime-import-src", 69 | srcs = glob([ 70 | "import/*.*", 71 | "import/core/*.*", 72 | "import/core/**/*.*", 73 | "import/etc/*.*", 74 | "import/etc/**/*.*", 75 | "import/ldc/*.*", 76 | "import/ldc/**/*.*", 77 | ]), 78 | ) 79 | """ 80 | 81 | def ldc2_archive(version, os, kernel, arch, ext): 82 | name = "ldc2_" + kernel + "_" + arch 83 | prefix = "ldc2-" + version + "-" + os + "-" + arch 84 | tarxz = prefix + ext 85 | return http_archive( 86 | name = name, 87 | urls = ["https://github.com/ldc-developers/ldc/releases/download/v" + version + "/" + tarxz], 88 | sha256 = _LDC2_SHA256SUMS[tarxz], 89 | strip_prefix=prefix, 90 | build_file_content = _LDC2_BUILD_FILE, 91 | ) 92 | 93 | def ldc2_repositories(version="1.28.0"): 94 | # TODO(karita): Support non x86_64 arch 95 | ldc2_archive(version, "linux", "linux", "x86_64", ".tar.xz") 96 | ldc2_archive(version, "osx", "darwin", "x86_64", ".tar.xz") 97 | ldc2_archive(version, "windows", "windows", "x64", ".7z") 98 | 99 | ldc2_compile_attrs = { 100 | "_ldc2_flag_version": attr.string(default = "--d-version"), 101 | "_ldc2_compiler": attr.label( 102 | default = Label("//rules:ldc2"), 103 | executable = True, 104 | allow_single_file = True, 105 | cfg = "host", 106 | ), 107 | "_ldc2_runtime_import_src": attr.label( 108 | default = Label("//rules:druntime-import-src-ldc2"), 109 | ), 110 | "_ldc2_stdlib": attr.label( 111 | default = Label("//rules:libphobos2-ldc2"), 112 | ), 113 | "_ldc2_stdlib_src": attr.label( 114 | default = Label("//rules:phobos-src-ldc2"), 115 | ), 116 | } 117 | -------------------------------------------------------------------------------- /source/kdr/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 kdr.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 | none, 20 | HP6, 21 | HP12, 22 | BP12, 23 | LP6, 24 | LP12, 25 | /// Moog ladder filter 26 | LP24, 27 | /// TB303 diode-ladder filter 28 | LPDL, 29 | } 30 | 31 | /// String names of filter implementations. 32 | static immutable filterNames = [__traits(allMembers, FilterKind)]; 33 | 34 | /// 35 | struct Filter { 36 | @nogc nothrow @safe pure: 37 | 38 | /// Applies filtering. 39 | /// Params: 40 | /// input = input wave frame. 41 | /// Returns: filtered wave frame. 42 | float apply(float input) { 43 | if (kind == FilterKind.none) return input; 44 | 45 | // TODO: use ring buffer 46 | foreach_reverse (i; 1 .. nFIR) { 47 | x[i] = x[i - 1]; 48 | } 49 | x[0] = input; 50 | 51 | float output = 0; 52 | foreach (i; 0 .. nFIR) { 53 | output += b[i] * x[i]; 54 | } 55 | foreach (i; 0 .. nIIR) { 56 | output -= a[i] * y[i]; 57 | } 58 | 59 | foreach_reverse (i; 1 .. nIIR) { 60 | y[i] = y[i - 1]; 61 | } 62 | y[0] = output; 63 | return output; 64 | } 65 | 66 | void setSampleRate(float sampleRate) { 67 | sampleRate = sampleRate; 68 | x[] = 0f; 69 | y[] = 0f; 70 | } 71 | 72 | /// Set filter parameters. 73 | /// Params: 74 | /// kind = filter type. 75 | /// freq = cutoff frequency [0, 1]. 76 | /// q = resonance, quality factor [0, 1]. 77 | void setParams(FilterKind kind, float freq, float q) { 78 | if (this.kind != kind) { 79 | x[] = 0f; 80 | y[] = 0f; 81 | } 82 | this.kind = kind; 83 | 84 | // To prevent the filter gets unstable. 85 | float Q; 86 | if (kind == FilterKind.LPDL) { 87 | // unstable at Q = 16 (see VAFD sec 5.10) 88 | Q = q * 15; 89 | freq += 0.005; // to prevent self osc. 90 | } 91 | else if (kind == FilterKind.LP24) { 92 | // unstable at Q = 4 (see VAFD sec 5.1, eq 5.2) 93 | Q = q * 3; 94 | freq += 0.005; // to prevent self osc. 95 | } 96 | else { 97 | Q = q * 5 + 1 / SQRT2; 98 | } 99 | const T = 1 / sampleRate; 100 | const w0 = 2 * PI * freq * sampleRate; 101 | assert(T != float.nan); 102 | assert(w0 != float.nan); 103 | final switch (kind) { 104 | case FilterKind.none: 105 | return; 106 | mixin(import("filter_coeff.d")); 107 | } 108 | } 109 | 110 | private: 111 | FilterKind kind = FilterKind.LP12; 112 | float sampleRate = 44_100; 113 | // filter and prev inputs 114 | float[5] b, x; 115 | // filter and prev outputs 116 | float[4] a, y; 117 | 118 | int nFIR = 3; 119 | int nIIR = 2; 120 | } 121 | 122 | unittest { 123 | Filter f; 124 | f.setSampleRate(20); 125 | f.setParams(FilterKind.LP12, 5, 2); 126 | 127 | // with padding 128 | auto y0 = f.apply(0.1); 129 | assert(approxEqual(y0, f.b[0] * 0.1)); 130 | 131 | auto y1 = f.apply(0.2); 132 | assert(approxEqual(y1, f.b[0] * 0.2 + f.b[1] * 0.1 - f.a[0] * y0)); 133 | 134 | auto y2 = f.apply(0.3); 135 | assert(approxEqual(y2, 136 | f.b[0] * 0.3 + f.b[1] * 0.2 + f.b[0] * 0.1 137 | -f.a[0] * y1 - f.a[1] * y0)); 138 | 139 | // without padding 140 | auto y3 = f.apply(0.4); 141 | assert(approxEqual(y3, 142 | f.b[0] * 0.4 + f.b[1] * 0.3 + f.b[0] * 0.2 143 | -f.a[0] * y2 - f.a[1] * y1)); 144 | } 145 | 146 | /// Single frame delayed all pass filter. 147 | struct AllPassFilter { 148 | @nogc nothrow pure @safe: 149 | 150 | float g = 0.5, py = 0, px = 0; 151 | 152 | void setSampleRate(float) { 153 | py = 0; 154 | px = 0; 155 | } 156 | 157 | float apply(float x) { 158 | const y = g * x + px - g * py; 159 | px = x; 160 | py = y; 161 | return y; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /source/kdr/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 kdr.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 kdr.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 | -------------------------------------------------------------------------------- /bin/freeverb/main.d: -------------------------------------------------------------------------------- 1 | /// Freeverb client based on faust implementation: https://github.com/grame-cncm/faustlibraries/blob/4cd48b91f1170498c1cf5d8ee5b87cda6cd797df/old/effect.lib#L1075 2 | import std.algorithm; 3 | import dplug.core; 4 | import dplug.client; 5 | import kdr.ringbuffer; 6 | 7 | struct AllPassFilter { 8 | /// Returns: the filtered output. 9 | /// Params: x = the signal input. 10 | @nogc nothrow pure 11 | float apply(const float x) { 12 | const float dx = _buffer.front; 13 | _buffer.enqueue(x + _coeff * dx); 14 | return dx - _coeff * (x + _coeff * dx); 15 | } 16 | 17 | private: 18 | float _coeff = 0.5; 19 | RingBuffer!float _buffer; 20 | alias _buffer this; 21 | } 22 | 23 | struct CombFilter { 24 | @nogc nothrow pure 25 | float apply(const float x, const float damp, const float feedback) { 26 | const float dx = _buffer.front; 27 | _last = dx * (1f - damp) + _last * damp; 28 | _buffer.enqueue(x + feedback * _last); 29 | return dx; 30 | } 31 | 32 | private: 33 | float _last = 0; 34 | RingBuffer!float _buffer; 35 | alias _buffer this; 36 | } 37 | 38 | class FreeverbClient : Client { 39 | public nothrow @nogc: 40 | 41 | this() {} 42 | 43 | override PluginInfo buildPluginInfo() { 44 | static immutable info = parsePluginInfo(import("plugin.json")); 45 | return info; 46 | } 47 | 48 | enum Params { damp, roomSize, wet, width, freeze } 49 | 50 | override Parameter[] buildParameters() { 51 | Vec!Parameter params = makeVec!Parameter(); 52 | params ~= mallocNew!LinearFloatParameter(Params.damp, "Damp", "", 0, 1, 0.5); 53 | params ~= mallocNew!LinearFloatParameter(Params.roomSize, "RoomSize", "", 0, 1, 0.5); 54 | params ~= mallocNew!LinearFloatParameter(Params.wet, "Wet", "", 0, 1, 0.3333); 55 | params ~= mallocNew!LinearFloatParameter(Params.width, "Width", "", 0, 1, 0.5); 56 | params ~= mallocNew!BoolParameter(Params.freeze, "Freeze", false); 57 | return params.releaseData(); 58 | } 59 | 60 | override LegalIO[] buildLegalIO() { 61 | Vec!LegalIO io = makeVec!LegalIO(); 62 | io ~= LegalIO(1, 1); 63 | io ~= LegalIO(2, 2); 64 | return io.releaseData(); 65 | } 66 | 67 | override int maxFramesInProcess() { return 32; } 68 | 69 | override void reset(double sampleRate, int maxFrames, int numInputs, int numOutputs) { 70 | _sampleRate = sampleRate; 71 | 72 | const int maxSpread = _widthToDelay(1.0); 73 | const maxCombDelay = cast(int) (combTunings[$ - 1] * sampleRate / _origSampleRate + maxSpread); 74 | foreach (int ch; 0 .. numInputs) { 75 | foreach (ref CombFilter f; _comb[ch]) { 76 | f.recalloc(maxCombDelay); 77 | } 78 | foreach (ref AllPassFilter f; _allPass[ch]) { 79 | f.recalloc(_maxAllPassDelay); 80 | } 81 | } 82 | } 83 | 84 | override void processAudio(const(float*)[] inputs, float*[] outputs, int frames, TimeInfo info) { 85 | _setFilterDelay(); 86 | 87 | const bool freeze = readParam!bool(Params.freeze); 88 | const float damp = freeze ? 0 : readParam!float(Params.damp) * 0.4 * _origSampleRate / _sampleRate; 89 | const float roomSize = freeze ? 1 : readParam!float(Params.roomSize) * 0.28 * _origSampleRate / _sampleRate + 0.7; 90 | const float wet = readParam!float(Params.wet); 91 | 92 | foreach (ch; 0 .. outputs.length) { 93 | foreach (t; 0 .. frames) { 94 | float x = freeze ? 0 : wet * 0.1 * (inputs[0][t] + inputs[1][t]); 95 | float y = 0; 96 | foreach (ref CombFilter f; _comb[ch]) { 97 | y += f.apply(x, damp, roomSize); 98 | } 99 | foreach (ref AllPassFilter f; _allPass[ch]) { 100 | y = f.apply(y); 101 | } 102 | outputs[ch][t] = y + (1 - wet) * inputs[ch][t]; 103 | } 104 | } 105 | } 106 | 107 | private: 108 | int _widthToDelay(const float width) const { 109 | return cast(int) (width * 46 * _sampleRate / _origSampleRate); 110 | } 111 | 112 | void _setFilterDelay() { 113 | const int width = _widthToDelay(readParam!float(Params.width)); 114 | 115 | foreach (ch; 0 .. 2) { 116 | const int spread = ch == 0 ? 0 : width; 117 | foreach (i, ref CombFilter f; _comb[ch]) { 118 | f.resize(cast(int) (combTunings[i] * _sampleRate / _origSampleRate) + spread); 119 | } 120 | foreach (i, ref AllPassFilter f; _allPass[ch]) { 121 | const int delay = cast(int) (allPassTunings[i] * _sampleRate / _origSampleRate); 122 | f.resize(min(_maxAllPassDelay, max(0, delay + spread - 1))); 123 | } 124 | } 125 | } 126 | 127 | AllPassFilter[2][4] _allPass; 128 | CombFilter[2][8] _comb; 129 | double _sampleRate = _origSampleRate; 130 | 131 | static immutable combTunings = [ 1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617 ]; 132 | static immutable allPassTunings = [ 556, 441, 341, 225 ]; 133 | enum _origSampleRate = 44_100; 134 | enum _maxAllPassDelay = 1024; 135 | } 136 | 137 | mixin(pluginEntryPoints!FreeverbClient); 138 | -------------------------------------------------------------------------------- /source/kdr/envtool/params.d: -------------------------------------------------------------------------------- 1 | module kdr.envtool.params; 2 | 3 | import dplug.math : vec2f; 4 | import dplug.core; 5 | import dplug.client; 6 | 7 | import kdr.envelope; 8 | import kdr.filter; 9 | 10 | /// Envelope mod destination. 11 | enum Destination { 12 | volume, 13 | pan, 14 | cutoff, 15 | } 16 | 17 | /// String names of destinations. 18 | static immutable destinationNames = [__traits(allMembers, Destination)]; 19 | 20 | 21 | /// Parameter for EnvToolClient. 22 | enum Params { 23 | envelope, 24 | bias = 4 * (Envelope.MAX_POINTS - 2), // -2 as begin/end uses bias. 25 | rate, 26 | depth, 27 | stereoOffset, 28 | destination, 29 | filterKind, 30 | filterCutoff, 31 | filterRes, 32 | } 33 | 34 | /// Used by the "rate" param. 35 | immutable string[] rateLabels = ["1/64", "1/48", "1/32", "1/24", "1/16", "1/12", "1/8", "1/6", "1/4", "1/3", "1/2", "1/1", "2/1", "4/1", "8/1"]; 36 | /// ditto. 37 | immutable double[] rateValues = [1./64, 1./48, 1./32, 1./24, 1./16, 1./12, 1./8, 1./6, 1./4, 1./3., 1./2, 1., 2., 4., 8.]; 38 | static assert(rateLabels.length == rateValues.length); 39 | 40 | /// Returns: 41 | /// Envelope parameters. 42 | @nogc nothrow 43 | Parameter[] buildEnvelopeParameters() { 44 | Vec!Parameter params; 45 | 46 | int n = 0; 47 | // Envelope config. 48 | // -2 for begin/end points. 49 | foreach (i; 0 .. Envelope.MAX_POINTS - 2) { 50 | if (i == 0) { 51 | params.pushBack(mallocNew!BoolParameter(n++, "enabled", true)); 52 | params.pushBack(mallocNew!LinearFloatParameter(n++, "x", "", 0, 1, 0.5)); 53 | params.pushBack(mallocNew!LinearFloatParameter(n++, "y", "", 0, 1, 1.0)); 54 | params.pushBack(mallocNew!BoolParameter(n++, "curve", false)); 55 | continue; 56 | } 57 | params.pushBack(mallocNew!BoolParameter(n++, "enabled", false)); 58 | params.pushBack(mallocNew!LinearFloatParameter(n++, "x", "", 0, 1, 0)); 59 | params.pushBack(mallocNew!LinearFloatParameter(n++, "y", "", 0, 1, 0)); 60 | params.pushBack(mallocNew!BoolParameter(n++, "curve", false)); 61 | } 62 | assert(n == Params.bias); 63 | params.pushBack(mallocNew!LinearFloatParameter(n++, "bias", "", 0, 1, 0)); 64 | 65 | // General config. 66 | assert(n == Params.rate); 67 | params.pushBack(mallocNew!EnumParameter(n++, "rate", rateLabels, 8)); 68 | assert(n == Params.depth); 69 | params.pushBack(mallocNew!LinearFloatParameter(n++, "depth", "", 0.0, 1.0, 1.0)); 70 | assert(n == Params.stereoOffset); 71 | params.pushBack(mallocNew!LinearFloatParameter(n++, "stereoOffset", "", -1, 1, 0.0)); 72 | assert(n == Params.destination); 73 | params.pushBack(mallocNew!EnumParameter(n++, "destination", destinationNames, Destination.volume)); 74 | assert(n == Params.filterKind); 75 | params.pushBack(mallocNew!EnumParameter(n++, "filterKind", filterNames, FilterKind.none)); 76 | assert(n == Params.filterCutoff); 77 | params.pushBack(mallocNew!LogFloatParameter(n++, "filterCutoff", "", 0.01, 1, 1)); 78 | assert(n == Params.filterRes); 79 | params.pushBack(mallocNew!LinearFloatParameter(n++, "filterRes", "", 0, 1, 0)); 80 | 81 | return params.releaseData(); 82 | } 83 | 84 | /// Params: 85 | /// params = type-erased parameters. 86 | /// Returns: 87 | /// a bias parameter of envelope. 88 | @nogc nothrow 89 | LinearFloatParameter envelopeBiasParam(Parameter[] params) { 90 | return cast(LinearFloatParameter) params[Params.bias]; 91 | } 92 | 93 | /// Value represents envelope point parameters. See also kdr.envelope.Envelope.Point. 94 | struct EnvelopePointParams { 95 | /// 96 | BoolParameter enabled; 97 | /// 98 | LinearFloatParameter x, y; 99 | /// 100 | BoolParameter curve; 101 | } 102 | 103 | /// Params: 104 | /// i = index of querying envelope point. 105 | /// params = type-erased parameters, which starts with the bias parameter. 106 | /// Returns: 107 | /// EnvelopePointParams at the given index i in mixed params. 108 | @nogc nothrow 109 | EnvelopePointParams envelopePointParamsAt(int i, Parameter[] params) { 110 | assert(i > 0); 111 | assert(i + 1 < Envelope.MAX_POINTS); 112 | int n = (i - 1) * 4; // cast(int) EnvelopePointParams.tupleof.length; 113 | BoolParameter enabled = cast(BoolParameter) params[n++]; 114 | LinearFloatParameter x = cast(LinearFloatParameter) params[n++]; 115 | LinearFloatParameter y = cast(LinearFloatParameter) params[n++]; 116 | BoolParameter curve = cast(BoolParameter) params[n++]; 117 | return EnvelopePointParams(enabled, x, y, curve); 118 | } 119 | 120 | /// Params: 121 | /// params = type-erased parameters, which starts with the bias parameter. 122 | /// Returns: 123 | /// Instantiated Envelope object. 124 | @nogc nothrow 125 | Envelope buildEnvelope(Parameter[] params) { 126 | Envelope ret; 127 | 128 | LinearFloatParameter bias = envelopeBiasParam(params); 129 | ret[0].y = bias.value; 130 | ret[$-1].y = bias.value; 131 | 132 | // 1 .. $-1 for skipping begin/end points. 133 | foreach (i; 1 .. Envelope.MAX_POINTS - 1) { 134 | EnvelopePointParams point = envelopePointParamsAt(i, params); 135 | if (point.enabled.value) { 136 | ret.add(Envelope.Point(vec2f(point.x.value, point.y.value), point.curve.value)); 137 | } 138 | } 139 | return ret; 140 | } 141 | 142 | // Envelope initializeEnvelope() 143 | -------------------------------------------------------------------------------- /resource/filter_coeff.d: -------------------------------------------------------------------------------- 1 | // -*- mode: d -*- 2 | // DON'T MODIFY THIS FILE AS GENERATED BY resource/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 | -------------------------------------------------------------------------------- /source/kdr/compressor.d: -------------------------------------------------------------------------------- 1 | /// Compressor module. 2 | /// 3 | /// Reference: 4 | /// [1] Digital Dynamic Range Compressor Design — A Tutorial and Analysis 5 | /// https://www.eecs.qmul.ac.uk/%7Ejosh/documents/2012/GiannoulisMassbergReiss-dynamicrangecompression-JAES2012.pdf 6 | module kdr.compressor; 7 | 8 | import std.math : isClose, log1p; 9 | 10 | import mir.math : exp, sqrt; 11 | 12 | import kdr.ringbuffer; 13 | 14 | struct RmsSlidingWindow { 15 | @nogc nothrow: 16 | 17 | this(int frames) { 18 | _buffer.recalloc(frames); 19 | } 20 | 21 | void clear() { 22 | _buffer.clear(); 23 | } 24 | 25 | float opCall(float x) { 26 | _buffer.enqueue(x * x); 27 | float sum = 0; 28 | foreach (sq; _buffer) { 29 | sum += sq; 30 | } 31 | return sqrt(sum / _buffer.length); 32 | } 33 | 34 | private: 35 | RingBuffer!float _buffer; 36 | } 37 | 38 | unittest { 39 | RmsSlidingWindow rms = 2; 40 | assert(rms(1) == sqrt(1f / 2)); 41 | assert(rms(2) == sqrt((1f + 4f) / 2)); 42 | assert(rms(3) == sqrt((4f + 9f) / 2)); 43 | } 44 | 45 | enum Knee { 46 | /// Hard knee without control vars. 47 | hard, 48 | /// Square curve knee with a width var. 49 | square, 50 | /// Soft-plus curve knee with a slope var. 51 | softplus, 52 | } 53 | 54 | struct GainCompressor { 55 | @nogc nothrow: 56 | 57 | Knee kneeKind = Knee.softplus; 58 | float kneeFactor = 0.5; // Assume in [0, 1]. Smaller gets harder. 59 | float upwardRatio = 1; 60 | float downwardRatio = 1; 61 | float upwardThreshold = 0; 62 | float downwardThreshold = float.max; 63 | float eps = 1e-6; 64 | 65 | float compress(float x) const pure { 66 | final switch (kneeKind) { 67 | case Knee.hard: return compressHardUp(compressHardDown(x)); 68 | case Knee.square: return compressSquareUp(compressSquareDown(x)); 69 | case Knee.softplus: return compressSoftPlusUp(compressSoftPlusDown(x)); 70 | } 71 | } 72 | 73 | float compressHardDown(float x) const pure { 74 | if (x > downwardThreshold) { 75 | return (x - downwardThreshold) / downwardRatio + downwardThreshold; 76 | } 77 | return x; 78 | } 79 | 80 | float compressHardUp(float x) const pure { 81 | if (x < upwardThreshold) { 82 | return (x - upwardThreshold) / upwardRatio + upwardThreshold; 83 | } 84 | return x; 85 | } 86 | 87 | /// Defined as Eq. (4) in [1]. 88 | /// It can be derived by solving these diff eqs: 89 | /// dy/dx = 1 if x < t - w/2 90 | /// = r if t + w/2 91 | /// = 1 + (r - 1) / w * (x - t + w/2) otherwise. 92 | /// The last eq is a linear interp btw 1st and 2nd eqs, 93 | /// where w is the width hyperparameter of the interp. 94 | float compressSquareDown(float x) const pure { 95 | float r = downwardRatio; 96 | float t = downwardThreshold; 97 | float w = kneeFactor * 50; 98 | if (x < t - w / 2) return x; 99 | if (t + w / 2 < x) return r * (x - t) + t; 100 | return x + (r - 1) * (x - t + w / 2) ^^ 2 / (2 * w + eps); 101 | } 102 | 103 | /// The upward version of compressSquareDown. 104 | /// It can be derived by solving these diff eqs: 105 | /// dy/dx = r if x < t - w/2 106 | /// = 1 if t + w/2 107 | /// = r + (1 - r) / w * (x - t + w/2) otherwise. 108 | /// The last eq is a linear interp btw 1st and 2nd eqs, 109 | /// where w is the width hyperparameter of the interp. 110 | float compressSquareUp(float x) const pure { 111 | float r = upwardRatio; 112 | float t = upwardThreshold; 113 | float w = kneeFactor * 50; 114 | if (x < t - w / 2) return r * (x - t) + t; 115 | if (t + w / 2 < x) return x; 116 | return r * x + (1 - r) * (x - t + w/2) ^^ 2 / (2 * w + eps) + (1 - r) * t; 117 | } 118 | 119 | /// This compressor has this smooth derivative around threshold t: 120 | /// y' = (r - 1) * sigmoid(a * (x - t)) + 1, 121 | /// Therefore, its output function is linear x + softplus at t. 122 | float compressSoftPlusDown(float x) const pure { 123 | float r = downwardRatio; 124 | float t = downwardThreshold; 125 | float a = (1 - kneeFactor) * 1000 + 0.1; 126 | float c = (1 - r) / a * log1p(exp(-a * t)); 127 | return (r - 1) / a * log1p(exp(a * (x - t))) + x + c; 128 | } 129 | 130 | /// This compressor has this smooth derivative around threshold t: 131 | /// y' = (1 - r) * sigmoid(a * (x - t)) + r, 132 | /// Therefore, its output function is linear x + softplus at t. 133 | float compressSoftPlusUp(float x) const pure { 134 | float r = upwardRatio; 135 | float t = upwardThreshold; 136 | float a = (1 - kneeFactor) * 1000 + 0.1; 137 | float c = (1 - r) * (t - log1p(exp(-a * t)) / a); 138 | return (1 - r) / a * log1p(exp(a * (x - t))) + r * x + c; 139 | } 140 | } 141 | 142 | // Check hard knee downward approx. 143 | unittest { 144 | GainCompressor comp; 145 | comp.kneeFactor = 0; 146 | comp.downwardRatio = 0.1; 147 | comp.downwardThreshold = 60; 148 | assert(isClose(comp.compressHardDown(60), 60)); 149 | assert(isClose(comp.compressSquareDown(60), 60)); 150 | assert(isClose(comp.compressSoftPlusDown(60), 60)); 151 | } 152 | 153 | // Check softknee smoothness. 154 | unittest { 155 | GainCompressor comp; 156 | enum eps = 1e-6; 157 | comp.kneeFactor = eps; 158 | comp.downwardRatio = 0.1; 159 | comp.downwardThreshold = 60; 160 | assert(comp.compressSquareDown(60) < 60); 161 | assert(comp.compressSoftPlusDown(60) < 60); 162 | assert(isClose(comp.compressSquareDown(60 + eps), 60)); 163 | assert(isClose(comp.compressSoftPlusDown(60 + eps), 60)); 164 | } 165 | 166 | // Check hard knee upward approx. 167 | unittest { 168 | GainCompressor comp; 169 | comp.kneeFactor = 0; 170 | comp.upwardRatio = 0.1; 171 | comp.upwardThreshold = 60; 172 | assert(isClose(comp.compressHardUp(60), 60)); 173 | assert(isClose(comp.compressSquareUp(60), 60)); 174 | assert(isClose(comp.compressSoftPlusUp(60), 60)); 175 | } 176 | 177 | // Check softknee smoothness. 178 | unittest { 179 | GainCompressor comp; 180 | enum eps = 1e-6; 181 | comp.kneeFactor = eps; 182 | comp.upwardRatio = 0.1; 183 | comp.upwardThreshold = 60; 184 | assert(comp.compressSquareUp(60) > 60); 185 | assert(comp.compressSoftPlusUp(60) > 60); 186 | assert(isClose(comp.compressSquareUp(60 + eps), 60)); 187 | assert(isClose(comp.compressSoftPlusUp(60 + eps), 60)); 188 | } 189 | -------------------------------------------------------------------------------- /source/kdr/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 kdr.oscillator; 9 | 10 | import dplug.client.midi : MidiMessage, MidiStatus; 11 | import mir.math : log2, exp2, fastmath, PI; 12 | 13 | import kdr.waveform : Waveform, WaveformRange; 14 | import kdr.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 | -------------------------------------------------------------------------------- /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="-kdr.envtool.params" 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 | -------------------------------------------------------------------------------- /source/kdr/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 kdr.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 kdr.waveform : Waveform, WaveformRange; 17 | import kdr.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/kdr/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 kdr.envelope; 8 | 9 | import std.algorithm : clamp; 10 | import std.math : isNaN; 11 | 12 | import dplug.math.vector : vec2f; 13 | import dplug.core.math : linmap; 14 | import dplug.client.midi : MidiMessage; 15 | import mir.math.common : fastmath; 16 | 17 | /// Envelope stages. 18 | enum Stage { 19 | attack, 20 | decay, 21 | sustain, 22 | release, 23 | done, 24 | } 25 | 26 | /// Attack, Decay, Sustain, Release. 27 | struct ADSR { 28 | /// Attack time in #frames. 29 | float attackTime = 0; 30 | /// Decay time in #frames. 31 | float decayTime = 0; 32 | /// Sustain level within [0, 1]. 33 | float sustainLevel = 1; 34 | /// Release time in #frames. 35 | float releaseTime = 0; 36 | 37 | @nogc nothrow @safe pure @fastmath: 38 | 39 | /// Triggers the atack stage. 40 | void attack() { 41 | _stage = Stage.attack; 42 | _stageTime = 0; 43 | } 44 | 45 | /// Triggers the release stage. 46 | void release() { 47 | _releaseLevel = this.front; 48 | _stage = Stage.release; 49 | _stageTime = 0; 50 | } 51 | 52 | void setSampleRate(float sampleRate) { 53 | _frameWidth = 1f / sampleRate; 54 | _stage = Stage.done; 55 | _stageTime = 0; 56 | _nplay = 0; 57 | } 58 | 59 | /// Returns: true if envelope was ended. 60 | bool empty() const { return _stage == Stage.done; } 61 | 62 | /// Returns: an amplitude of the linear envelope. 63 | float front() const { 64 | final switch (_stage) { 65 | case Stage.attack: 66 | return this.attackTime == 0 ? 1 : (_stageTime / this.attackTime); 67 | case Stage.decay: 68 | return this.decayTime == 0 69 | ? 1 : (_stageTime * (this.sustainLevel - 1f) / this.decayTime + 1f); 70 | case Stage.sustain: 71 | return this.sustainLevel; 72 | case Stage.release: 73 | assert(!isNaN(_releaseLevel), "invalid release level."); 74 | return this.releaseTime == 0 ? 0f 75 | : (-_stageTime * _releaseLevel / this.releaseTime 76 | + _releaseLevel); 77 | case Stage.done: 78 | return 0f; 79 | } 80 | } 81 | 82 | /// Update status if the stage is in (attack, decay, release). 83 | void popFront() { 84 | final switch (_stage) { 85 | case Stage.attack: 86 | _stageTime += _frameWidth; 87 | if (_stageTime >= this.attackTime) { 88 | _stage = Stage.decay; 89 | _stageTime = 0; 90 | } 91 | return; 92 | case Stage.decay: 93 | _stageTime += _frameWidth; 94 | if (_stageTime >= this.decayTime) { 95 | _stage = Stage.sustain; 96 | _stageTime = 0; 97 | } 98 | return; 99 | case Stage.sustain: 100 | return; // do nothing. 101 | case Stage.release: 102 | _stageTime += _frameWidth; 103 | if (_stageTime >= this.releaseTime) { 104 | _stage = Stage.done; 105 | _stageTime = 0; 106 | } 107 | return; 108 | case Stage.done: 109 | return; // do nothing. 110 | } 111 | } 112 | 113 | @system void setMidi(MidiMessage msg) { 114 | if (msg.isNoteOn) { 115 | if (_nplay == 0) this.attack(); 116 | ++_nplay; 117 | } 118 | if (msg.isNoteOff) { 119 | --_nplay; 120 | if (_nplay == 0) this.release(); 121 | } 122 | } 123 | 124 | private: 125 | Stage _stage = Stage.done; 126 | float _frameWidth = 1.0 / 44_100; 127 | float _stageTime = 0; 128 | float _releaseLevel; 129 | int _nplay = 0; 130 | } 131 | 132 | /// Test ADSR. 133 | @nogc nothrow pure @safe 134 | unittest { 135 | ADSR env; 136 | env.attackTime = 5; 137 | env.decayTime = 5; 138 | env.sustainLevel = 0.5; 139 | env.releaseTime = 20; 140 | env._frameWidth = 1; 141 | 142 | foreach (_; 0 .. 2) { 143 | env.attack(); 144 | foreach (i; 0 .. env.attackTime) { 145 | assert(env._stage == Stage.attack); 146 | env.popFront(); 147 | } 148 | foreach (i; 0 .. env.decayTime) { 149 | assert(env._stage == Stage.decay); 150 | env.popFront(); 151 | } 152 | assert(env._stage == Stage.sustain); 153 | env.release(); 154 | // foreach does not mutate `env`.N 155 | foreach (amp; env) { 156 | assert(env._stage == Stage.release); 157 | } 158 | foreach (i; 0 .. env.releaseTime) { 159 | assert(env._stage == Stage.release); 160 | env.popFront(); 161 | } 162 | assert(env._stage == Stage.done); 163 | assert(env.empty); 164 | assert(env.front == 0); 165 | } 166 | } 167 | 168 | /// Dynamically adjustable envelope shaper. 169 | struct Envelope { 170 | public: 171 | @nogc nothrow: 172 | 173 | enum MAX_POINTS = 32; 174 | 175 | float getY(float x) const pure @safe { 176 | assert(0 <= x && x <= 1); 177 | size_t nextIdx = newIndex(x); 178 | if (nextIdx == length) return _points[length - 1].y; 179 | 180 | size_t prevIdx = nextIdx - 1; 181 | Point prev = this[prevIdx]; 182 | Point next = this[nextIdx]; 183 | if (!prev.isCurve && !next.isCurve) { 184 | return linmap(x, prev.x, next.x, prev.y, next.y); 185 | } 186 | 187 | // ??? 188 | while (this[prevIdx].isCurve) --prevIdx; 189 | while (this[nextIdx].isCurve) ++nextIdx; 190 | return interpolate(x, this[prevIdx .. nextIdx + 1]); 191 | } 192 | 193 | bool add(Point p) pure @safe { 194 | assert(0 <= p.x && p.x <= 1); 195 | assert(0 <= p.y && p.y <= 1); 196 | 197 | // Cannot add x anymore. 198 | if (length >= MAX_POINTS) return false; 199 | 200 | const idx = newIndex(p.x); 201 | foreach_reverse (i; idx .. length) { 202 | _points[i + 1] = _points[i]; 203 | } 204 | _points[idx] = p; 205 | ++_length; 206 | return true; 207 | } 208 | 209 | bool del(int i) pure @safe { 210 | if (i <= 0 || length <= i) return false; 211 | 212 | foreach (j; i .. length) { 213 | _points[j] = _points[j + 1]; 214 | } 215 | --_length; 216 | return true; 217 | } 218 | 219 | int length() const pure @safe { return _length; } 220 | 221 | /// Value of evelope points. 222 | struct Point { 223 | @nogc nothrow pure @safe: 224 | vec2f xy; 225 | alias xy this; 226 | bool isCurve; 227 | } 228 | 229 | inout(Point)[] points() inout pure @safe return { 230 | return _points[0 .. length]; 231 | } 232 | 233 | /// For array-like (opIndex etc) overloading. 234 | alias points this; 235 | 236 | private: 237 | /// Params: newx = new x value to be added to points. 238 | /// Returns: a new index if newx will be added to xs. 239 | size_t newIndex(float newx) const pure @safe { 240 | foreach (i, p; _points[0 .. length]) { 241 | if (newx < p.x) { 242 | return i; 243 | } 244 | } 245 | return length - 1; 246 | } 247 | 248 | // Lagrange interpolation. 249 | float interpolate(float x, const Point[] ps) const pure @safe { 250 | float y = 0; 251 | foreach (i, p; ps) { 252 | float lx = 1; 253 | foreach (j, q; ps) { 254 | if (i == j) continue; 255 | lx *= (x - q.x) / (p.x - q.x); 256 | } 257 | y += p.y * lx; 258 | } 259 | return clamp(y, 0, 1); 260 | } 261 | 262 | int _length = 2; 263 | Point[MAX_POINTS] _points = [Point(vec2f(0, 0)), Point(vec2f(1, 0))]; 264 | } 265 | 266 | nothrow pure @safe 267 | unittest { 268 | Envelope env; 269 | 270 | // Initial start/end points. 271 | assert(env.getY(0.0) == 0.0); 272 | assert(env.getY(1.0) == 0.0); 273 | assert(env[0] == vec2f(0, 0)); 274 | assert(env[1] == vec2f(1, 0)); 275 | assert(env[$-1] == vec2f(1, 0)); 276 | 277 | // Check interp. 278 | assert(env.getY(0.25) == 0.0); 279 | assert(env.getY(0.75) == 0.0); 280 | 281 | // Add a new point. 282 | assert(env.add(Envelope.Point(vec2f(0.5, 1.0)))); 283 | assert(env.getY(0.5) == 1.0); 284 | assert(env[1] == vec2f(0.5, 1.0)); 285 | assert(env.length == 3); 286 | 287 | // The added point changes the interp. 288 | assert(env.getY(0.25) == 0.5); 289 | assert(env.getY(0.75) == 0.5); 290 | 291 | // Update the existing point. 292 | env[1] = Envelope.Point(vec2f(0, 0)); 293 | assert(env[1] == vec2f(0, 0)); 294 | 295 | assert(env.del(1)); 296 | assert(env.length == 2); 297 | } 298 | -------------------------------------------------------------------------------- /source/kdr/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 kdr.synth2.params; 8 | 9 | import dplug.core.nogc : destroyFree, mallocNew; 10 | import dplug.client.params : BoolParameter, EnumParameter, GainParameter, 11 | IntegerParameter, LinearFloatParameter, LogFloatParameter; 12 | import mir.math.constant : PI; 13 | 14 | import kdr.delay : DelayKind, delayNames; 15 | import kdr.effect : EffectKind, effectNames; 16 | import kdr.waveform : Waveform, waveformNames; 17 | import kdr.filter : filterNames, FilterKind; 18 | import kdr.lfo : Multiplier, multiplierNames; 19 | import kdr.params : RegisterBuilder; 20 | 21 | /// Parameter ids. 22 | @RegisterBuilder!ParamBuilder 23 | enum Params : int { 24 | /// Oscillator section 25 | osc1Waveform, 26 | osc1Det, 27 | osc1FM, 28 | 29 | osc2Waveform, 30 | osc2Ring, 31 | osc2Sync, 32 | osc2Track, 33 | osc2Pitch, 34 | osc2Fine, 35 | 36 | oscMix, 37 | oscKeyShift, 38 | oscTune, 39 | oscPhase, 40 | 41 | oscPulseWidth, 42 | oscSubWaveform, 43 | oscSubVol, 44 | oscSubOct, 45 | 46 | /// Amp section 47 | ampAttack, 48 | ampDecay, 49 | ampSustain, 50 | ampRelease, 51 | ampGain, 52 | ampVel, 53 | 54 | /// Filter section 55 | filterKind, 56 | filterCutoff, 57 | filterQ, 58 | filterTrack, 59 | filterAttack, 60 | filterDecay, 61 | filterSustain, 62 | filterRelease, 63 | filterEnvAmount, 64 | filterUseVelocity, 65 | saturation, 66 | 67 | /// Mod envelope 68 | menvDest, 69 | menvAttack, 70 | menvDecay, 71 | menvAmount, 72 | 73 | /// LFO1 74 | lfo1Wave, 75 | lfo1Dest, 76 | lfo1Sync, 77 | lfo1Speed, 78 | lfo1Mul, 79 | lfo1Amount, 80 | lfo1Trigger, 81 | 82 | /// LFO2 83 | lfo2Wave, 84 | lfo2Dest, 85 | lfo2Sync, 86 | lfo2Speed, 87 | lfo2Mul, 88 | lfo2Amount, 89 | lfo2Trigger, 90 | 91 | /// Effect 92 | // TODO: add effectOn param. 93 | effectKind, 94 | effectCtrl1, 95 | effectCtrl2, 96 | effectMix, 97 | 98 | // Equalizer / Pan 99 | eqFreq, 100 | eqLevel, 101 | eqQ, 102 | eqTone, 103 | eqPan, 104 | 105 | // Voice 106 | voiceKind, 107 | voicePoly, 108 | voicePortament, 109 | voicePortamentAuto, 110 | 111 | // Delay 112 | // TODO: add delayOn param. 113 | delayKind, 114 | delayTime, 115 | delayMul, 116 | delaySpread, 117 | delayFeedback, 118 | delayTone, 119 | delayMix, 120 | 121 | // Chorus/flanger 122 | chorusOn, 123 | chorusMulti, 124 | chorusTime, 125 | chorusDepth, 126 | chorusRate, 127 | chorusFeedback, 128 | chorusLevel, 129 | chorusWidth, 130 | } 131 | 132 | static immutable paramNames = [__traits(allMembers, Params)]; 133 | 134 | /// Modulation envelope destination. 135 | enum MEnvDest { 136 | osc2, 137 | fm, 138 | pw, 139 | } 140 | 141 | static immutable menvDestNames = [__traits(allMembers, MEnvDest)]; 142 | 143 | /// LFO modulation destination. 144 | enum LfoDest { 145 | osc2, 146 | osc12, 147 | filter, 148 | amp, 149 | pw, 150 | fm, 151 | pan, 152 | } 153 | 154 | static immutable lfoDestNames = [__traits(allMembers, LfoDest)]; 155 | 156 | /// Voice kind. 157 | enum VoiceKind { 158 | poly, 159 | mono, 160 | legato, 161 | } 162 | 163 | enum maxPoly = 16; 164 | static immutable voiceKindNames = [__traits(allMembers, VoiceKind)]; 165 | 166 | /// Setup default parameter. 167 | struct ParamBuilder { 168 | 169 | static osc1Waveform() { 170 | return mallocNew!EnumParameter( 171 | Params.osc1Waveform, "Osc1/Wave", waveformNames, Waveform.sine); 172 | } 173 | 174 | static osc1Det() { 175 | return mallocNew!LinearFloatParameter( 176 | Params.osc1Det, "Osc1/Detune", "", 0, 1, 0); 177 | } 178 | 179 | static osc1FM() { 180 | return mallocNew!LinearFloatParameter( 181 | Params.osc1FM, "Osc1/FM", "", 0, 10, 0); 182 | } 183 | 184 | static osc2Waveform() { 185 | return mallocNew!EnumParameter( 186 | Params.osc2Waveform, "Osc2/Wave", waveformNames, Waveform.triangle); 187 | } 188 | 189 | static osc2Track() { 190 | return mallocNew!BoolParameter(Params.osc2Track, "Osc2/Track", true); 191 | } 192 | 193 | // TODO: check synth1 default (440hz?) 194 | static osc2Pitch() { 195 | return mallocNew!IntegerParameter( 196 | Params.osc2Pitch, "Osc2/Pitch", "", -12, 12, 0); 197 | } 198 | 199 | static osc2Fine() { 200 | return mallocNew!LinearFloatParameter( 201 | Params.osc2Fine, "Osc2/Fine", "", -1.0, 1.0, 0.0); 202 | } 203 | 204 | static osc2Ring() { 205 | return mallocNew!BoolParameter(Params.osc2Ring, "Osc2/Ring", false); 206 | } 207 | 208 | static osc2Sync() { 209 | return mallocNew!BoolParameter(Params.osc2Sync, "Osc2/Sync", false); 210 | } 211 | 212 | static oscMix() { 213 | return mallocNew!LinearFloatParameter( 214 | Params.oscMix, "Osc/Mix", "", 0f, 1f, 0f); 215 | } 216 | 217 | static oscKeyShift() { 218 | return mallocNew!IntegerParameter( 219 | Params.oscKeyShift, "Osc/KeyShift", "semitone", -12, 12, 0); 220 | } 221 | 222 | static oscTune() { 223 | return mallocNew!LinearFloatParameter( 224 | Params.oscTune, "Osc/Tune", "cent", -1.0, 1.0, 0); 225 | } 226 | 227 | enum float ignoreOscPhase = -PI; 228 | 229 | static oscPhase() { 230 | return mallocNew!LinearFloatParameter( 231 | Params.oscPhase, "Osc/Phase", "", -PI, PI, ignoreOscPhase); 232 | } 233 | 234 | static oscPulseWidth() { 235 | return mallocNew!LinearFloatParameter( 236 | Params.oscPulseWidth, "Osc/PW", "", 0f, 1f, 0.5f); 237 | } 238 | 239 | static oscSubWaveform() { 240 | return mallocNew!EnumParameter( 241 | Params.oscSubWaveform, "OscSub/Wave", waveformNames, Waveform.sine); 242 | } 243 | 244 | // TODO: check synth1 max vol. 245 | static oscSubVol() { 246 | return mallocNew!GainParameter( 247 | Params.oscSubVol, "OscSub/Gain", 3, -float.infinity); 248 | } 249 | 250 | static oscSubOct() { 251 | return mallocNew!BoolParameter(Params.oscSubOct, "OscSub/-1Oct", false); 252 | } 253 | 254 | // Epsilon value to avoid NaN in log. 255 | enum logBias = 1e-3; 256 | 257 | static ampAttack() { 258 | return mallocNew!LogFloatParameter( 259 | Params.ampAttack, "Amp/Attack", "sec", logBias, 100.0, logBias); 260 | } 261 | 262 | static ampDecay() { 263 | return mallocNew!LogFloatParameter( 264 | Params.ampDecay, "Amp/Decay", "sec", logBias, 100.0, logBias); 265 | } 266 | 267 | static ampSustain() { 268 | return mallocNew!GainParameter( 269 | Params.ampSustain, "Amp/Sustain", 0.0, 0.0); 270 | } 271 | 272 | static ampRelease() { 273 | return mallocNew!LogFloatParameter( 274 | Params.ampRelease, "Amp/Release", "sec", logBias, 100, logBias); 275 | } 276 | 277 | static ampGain() { 278 | return mallocNew!GainParameter(Params.ampGain, "Amp/Gain", 3.0, 0.0); 279 | } 280 | 281 | static ampVel() { 282 | return mallocNew!LinearFloatParameter( 283 | Params.ampVel, "Amp/Velocity", "", 0, 1.0, 1.0); 284 | } 285 | 286 | static filterKind() { 287 | return mallocNew!EnumParameter( 288 | Params.filterKind, "Filter/Kind", filterNames, FilterKind.LP12); 289 | } 290 | 291 | static filterCutoff() { 292 | return mallocNew!LogFloatParameter( 293 | Params.filterCutoff, "Filter/Cutoff", "", logBias, 1, 1); 294 | } 295 | 296 | static filterQ() { 297 | return mallocNew!LinearFloatParameter( 298 | Params.filterQ, "Filter/Q", "", 0, 1, 0); 299 | } 300 | 301 | static filterTrack() { 302 | return mallocNew!LinearFloatParameter( 303 | Params.filterTrack, "Filter/Track", "", 0, 1, 0); 304 | } 305 | 306 | static filterEnvAmount() { 307 | return mallocNew!LinearFloatParameter( 308 | Params.filterEnvAmount, "Filter/Amount", "", 0, 1, 0); 309 | } 310 | 311 | static filterAttack() { 312 | return mallocNew!LogFloatParameter( 313 | Params.filterAttack, "Filter/Attack", "sec", logBias, 100.0, logBias); 314 | } 315 | 316 | static filterDecay() { 317 | return mallocNew!LogFloatParameter( 318 | Params.filterDecay, "Filter/Decay", "sec", logBias, 100.0, logBias); 319 | } 320 | 321 | static filterSustain() { 322 | return mallocNew!GainParameter( 323 | Params.filterSustain, "Filter/Sustain", 0.0, 0.0); 324 | } 325 | 326 | static filterRelease() { 327 | return mallocNew!LogFloatParameter( 328 | Params.filterRelease, "Filter/Release", "sec", logBias, 100, logBias); 329 | } 330 | 331 | static filterUseVelocity() { 332 | return mallocNew!BoolParameter( 333 | Params.filterUseVelocity, "Filter/Velocity", false); 334 | } 335 | 336 | static saturation() { 337 | return mallocNew!LinearFloatParameter( 338 | Params.saturation, "Saturation", "", 0, 100, 0); 339 | } 340 | 341 | static menvDest() { 342 | return mallocNew!EnumParameter( 343 | Params.menvDest, "MEnv/Destination", menvDestNames, MEnvDest.osc2); 344 | } 345 | 346 | static menvAttack() { 347 | return mallocNew!LogFloatParameter( 348 | Params.menvAttack, "MEnv/Attack", "sec", logBias, 100.0, logBias); 349 | } 350 | 351 | static menvDecay() { 352 | return mallocNew!LogFloatParameter( 353 | Params.menvDecay, "MEnv/Decay", "sec", logBias, 100.0, logBias); 354 | } 355 | 356 | static menvAmount() { 357 | return mallocNew!LinearFloatParameter( 358 | Params.menvAmount, "MEnv/Amount", "", -100, 100, 0); 359 | } 360 | 361 | static effectKind() { 362 | return mallocNew!EnumParameter( 363 | Params.effectKind, "Effect/Kind", effectNames, EffectKind.ad1); 364 | } 365 | 366 | static effectCtrl1() { 367 | return mallocNew!LinearFloatParameter( 368 | Params.effectCtrl1, "Effect/Ctrl1", "", 0, 1, 0.5); 369 | } 370 | 371 | static effectCtrl2() { 372 | return mallocNew!LinearFloatParameter( 373 | Params.effectCtrl2, "Effect/Ctrl2", "", 0, 1, 0.5); 374 | } 375 | 376 | static effectMix() { 377 | return mallocNew!LinearFloatParameter( 378 | Params.effectMix, "Effect/Mix", "", 0, 1, 0); 379 | } 380 | 381 | static lfo1Wave() { 382 | return mallocNew!EnumParameter( 383 | Params.lfo1Wave, "LFO1/Wave", waveformNames, Waveform.triangle); 384 | } 385 | 386 | static lfo1Dest() { 387 | return mallocNew!EnumParameter( 388 | Params.lfo1Dest, "LFO1/Dest", lfoDestNames, LfoDest.osc12); 389 | } 390 | 391 | static lfo1Sync() { 392 | return mallocNew!BoolParameter(Params.lfo1Sync, "LFO1/Sync", true); 393 | } 394 | 395 | static lfo1Speed() { 396 | return mallocNew!LinearFloatParameter( 397 | Params.lfo1Speed, "LFO1/Speed", "", 0, 1, 0.5); 398 | } 399 | 400 | static lfo1Mul() { 401 | return mallocNew!EnumParameter( 402 | Params.lfo1Mul, "LFO1/Mul", multiplierNames, Multiplier.none); 403 | } 404 | 405 | static lfo1Amount() { 406 | return mallocNew!LinearFloatParameter( 407 | Params.lfo1Amount, "LFO1/Amount", "", 0, 1, 0); 408 | } 409 | 410 | static lfo1Trigger() { 411 | return mallocNew!BoolParameter(Params.lfo1Trigger, "LFO1/trigger", false); 412 | } 413 | 414 | static lfo2Wave() { 415 | return mallocNew!EnumParameter( 416 | Params.lfo2Wave, "LFO2/Wave", waveformNames, Waveform.triangle); 417 | } 418 | 419 | static lfo2Dest() { 420 | return mallocNew!EnumParameter( 421 | Params.lfo2Dest, "LFO2/Dest", lfoDestNames, LfoDest.osc12); 422 | } 423 | 424 | static lfo2Sync() { 425 | return mallocNew!BoolParameter(Params.lfo2Sync, "LFO2/Sync", true); 426 | } 427 | 428 | static lfo2Speed() { 429 | return mallocNew!LinearFloatParameter( 430 | Params.lfo2Speed, "LFO2/Speed", "", 0, 1, 0.5); 431 | } 432 | 433 | static lfo2Mul() { 434 | return mallocNew!EnumParameter( 435 | Params.lfo2Mul, "LFO2/Mul", multiplierNames, Multiplier.none); 436 | } 437 | 438 | static lfo2Amount() { 439 | return mallocNew!LinearFloatParameter( 440 | Params.lfo2Amount, "LFO2/Amount", "", 0, 1, 0); 441 | } 442 | 443 | static lfo2Trigger() { 444 | return mallocNew!BoolParameter(Params.lfo2Trigger, "LFO2/Trigger", false); 445 | } 446 | 447 | static eqFreq() { 448 | return mallocNew!LogFloatParameter(Params.eqFreq, "EQ/Freq", "", logBias, 1, 0.5); 449 | } 450 | 451 | static eqLevel() { 452 | return mallocNew!LinearFloatParameter(Params.eqLevel, "EQ/Level", "", -1, 1, 0); 453 | } 454 | 455 | static eqQ() { 456 | return mallocNew!LinearFloatParameter(Params.eqQ, "EQ/Q", "", 0, 1, 0); 457 | } 458 | 459 | static eqTone() { 460 | return mallocNew!LinearFloatParameter(Params.eqTone, "EQ/Tone", "", -1, 1, 0); 461 | } 462 | 463 | static eqPan() { 464 | return mallocNew!LinearFloatParameter(Params.eqPan, "EQ/Pan", "", -1, 1, 0); 465 | } 466 | 467 | static voiceKind() { 468 | return mallocNew!EnumParameter( 469 | Params.voiceKind, "Voice/Kind", voiceKindNames, VoiceKind.poly); 470 | } 471 | 472 | static voicePoly() { 473 | return mallocNew!IntegerParameter( 474 | Params.voicePoly, "Voice/Poly", "voices", 0, maxPoly, maxPoly); 475 | } 476 | 477 | static voicePortament() { 478 | return mallocNew!LogFloatParameter( 479 | Params.voicePortament, "Voice/Port", "sec", logBias, 1, logBias); 480 | } 481 | 482 | static voicePortamentAuto() { 483 | return mallocNew!BoolParameter(Params.voicePortamentAuto, "Voice/Auto", true); 484 | } 485 | 486 | static delayKind() { 487 | return mallocNew!EnumParameter( 488 | Params.delayKind, "DelayKind", delayNames, DelayKind.st); 489 | } 490 | 491 | static delayTime() { 492 | return mallocNew!LinearFloatParameter( 493 | Params.delayTime, "Delay/Time", "", 0, 1, 1); 494 | } 495 | 496 | static delayMul() { 497 | return mallocNew!EnumParameter( 498 | Params.delayMul, "Delay/Mul", multiplierNames, Multiplier.none); 499 | } 500 | 501 | static delaySpread() { 502 | return mallocNew!LinearFloatParameter( 503 | Params.delaySpread, "Delay/Spread", "sec", 0, 0.1, 0); 504 | } 505 | 506 | static delayFeedback() { 507 | return mallocNew!LinearFloatParameter( 508 | Params.delayFeedback, "Delay/Feedback", "", 0, 1, 0); 509 | } 510 | 511 | static delayTone() { 512 | return mallocNew!LinearFloatParameter( 513 | Params.delayTone, "Delay/Tone", "", -1, 1, 0); 514 | } 515 | 516 | static delayMix() { 517 | return mallocNew!LinearFloatParameter( 518 | Params.delayMix, "Delay/Mix", "", 0, 1, 0); 519 | } 520 | 521 | static chorusOn() { 522 | return mallocNew!BoolParameter(Params.chorusOn, "Chrous/On", false); 523 | } 524 | 525 | static chorusMulti() { 526 | return mallocNew!IntegerParameter( 527 | Params.chorusMulti, "Chorus/Multi", "mul", 1, 4, 1); 528 | } 529 | 530 | static chorusTime() { 531 | return mallocNew!LinearFloatParameter( 532 | Params.chorusTime, "Chorus/Time", "ms", 3, 40, 11.1); 533 | } 534 | 535 | static chorusDepth() { 536 | return mallocNew!LinearFloatParameter( 537 | Params.chorusDepth, "Chorus/Depth", "", 0, 0.5, 0.25); 538 | } 539 | 540 | static chorusRate() { 541 | return mallocNew!LogFloatParameter( 542 | Params.chorusRate, "Chorus/Rate", "hz", logBias, 20, 0.86); 543 | } 544 | 545 | static chorusFeedback() { 546 | return mallocNew!LinearFloatParameter( 547 | Params.chorusFeedback, "Chorus/Feedback", "", 0, 1, 0); 548 | } 549 | 550 | static chorusLevel() { 551 | return mallocNew!GainParameter( 552 | Params.chorusLevel, "Chorus/Level", 5, 0.28); 553 | } 554 | 555 | static chorusWidth() { 556 | return mallocNew!LinearFloatParameter( 557 | Params.chorusWidth, "Chorus/Width", "", 0, 1, 0); 558 | } 559 | } 560 | -------------------------------------------------------------------------------- /rules/d.bzl: -------------------------------------------------------------------------------- 1 | # Copyright 2022 klknn. 2 | # Copyright 2015 The Bazel Authors. All rights reserved 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """D rules for Bazel.""" 17 | load(":d_toolchain.bzl", "d_toolchain", "d_toolchain_attrs") 18 | 19 | COMPILATION_MODE_FLAGS_POSIX = { 20 | "fastbuild": ["-g"], 21 | "dbg": ["-debug", "-g"], 22 | "opt": ["-checkaction=halt", "-boundscheck=safeonly", "-O"], 23 | } 24 | 25 | COMPILATION_MODE_FLAGS_WINDOWS = { 26 | "fastbuild": ["-g", "-m64", "-mscrtlib=msvcrt"], 27 | "dbg": ["-debug", "-g", "-m64", "-mscrtlib=msvcrtd"], 28 | "opt": ["-checkaction=halt", "-boundscheck=safeonly", "-O", 29 | "-m64", "-mscrtlib=msvcrt"], 30 | } 31 | 32 | def _is_windows(ctx): 33 | return ctx.configuration.host_path_separator == ";" 34 | 35 | def _compilation_mode_flags(ctx): 36 | """Returns a list of flags based on the compilation_mode.""" 37 | if _is_windows(ctx): 38 | return COMPILATION_MODE_FLAGS_WINDOWS[ctx.var["COMPILATION_MODE"]] 39 | else: 40 | return COMPILATION_MODE_FLAGS_POSIX[ctx.var["COMPILATION_MODE"]] 41 | 42 | def _format_version(name): 43 | """Formats the string name to be used in a --version flag.""" 44 | return name.replace("-", "_") 45 | 46 | def _build_import(label, im): 47 | """Builds the import path under a specific label""" 48 | import_path = "" 49 | if label.workspace_root: 50 | import_path += label.workspace_root + "/" 51 | if label.package: 52 | import_path += label.package + "/" 53 | import_path += im 54 | return import_path 55 | 56 | def _files_directory(files): 57 | """Returns the shortest parent directory of a list of files.""" 58 | dir = files[0].dirname 59 | for f in files: 60 | if len(dir) > len(f.dirname): 61 | dir = f.dirname 62 | return dir 63 | 64 | def _build_compile_arglist(ctx, out, depinfo, imports, string_imports, extra_flags = []): 65 | """Returns a list of strings constituting the D compile command arguments.""" 66 | toolchain = d_toolchain(ctx) 67 | return ( 68 | _compilation_mode_flags(ctx) + 69 | extra_flags + [ 70 | "-of" + out.path, 71 | "-I.", 72 | "-w", 73 | ] + 74 | ["-I%s" % _build_import(ctx.label, im) for im in ctx.attr.imports] + 75 | ["-I%s" % im for im in imports] + 76 | ["-I" + _files_directory(toolchain.stdlib_src), 77 | "-I" + _files_directory(toolchain.runtime_import_src)] + 78 | ["%s=Have_%s" % (toolchain.flag_version, _format_version(ctx.label.name))] + 79 | ["%s=%s" % (toolchain.flag_version, v) for v in ctx.attr.versions] + 80 | ["%s=%s" % (toolchain.flag_version, v) for v in depinfo.versions] + 81 | ["-J=" + f.dirname for f in string_imports] 82 | ) 83 | 84 | def _build_link_arglist(ctx, objs, out, depinfo): 85 | """Returns a list of strings constituting the D link command arguments.""" 86 | toolchain = d_toolchain(ctx) 87 | return ( 88 | _compilation_mode_flags(ctx) + 89 | ["-of" + out.path] + 90 | [("-L/LIBPATH:" if _is_windows(ctx) else "-L-L") + toolchain.stdlib[0].dirname] + 91 | [f.path for f in depinfo.libs.to_list() + depinfo.transitive_libs.to_list()] + 92 | depinfo.link_flags + 93 | objs 94 | ) 95 | 96 | def _a_filetype(ctx, files): 97 | lib_suffix = ".lib" if _is_windows(ctx) else ".a" 98 | return [f for f in files if f.basename.endswith(lib_suffix)] 99 | 100 | def _setup_deps(ctx, deps, name, working_dir): 101 | """Sets up dependencies. 102 | 103 | Walks through dependencies and constructs the commands and flags needed 104 | for linking the necessary dependencies. 105 | 106 | Args: 107 | deps: List of deps labels from ctx.attr.deps. 108 | name: Name of the current target. 109 | working_dir: The output directory of the current target's output. 110 | 111 | Returns: 112 | Returns a struct containing the following fields: 113 | libs: List of Files containing the target's direct library dependencies. 114 | transitive_libs: List of Files containing all of the target's 115 | transitive libraries. 116 | d_srcs: List of Files representing D source files of dependencies that 117 | will be used as inputs for this target. 118 | versions: List of D versions to be used for compiling the target. 119 | imports: List of Strings containing input paths that will be passed 120 | to the D compiler via -I flags. 121 | link_flags: List of linker flags. 122 | """ 123 | libs = [] 124 | transitive_libs = [] 125 | d_srcs = [] 126 | transitive_d_srcs = [] 127 | versions = [] 128 | imports = [] 129 | link_flags = [] 130 | for dep in deps: 131 | if hasattr(dep, "d_lib"): 132 | # The dependency is a d_library. 133 | libs.append(dep.d_lib) 134 | transitive_libs.append(dep.transitive_libs) 135 | d_srcs += dep.d_srcs 136 | transitive_d_srcs.append(dep.transitive_d_srcs) 137 | versions += dep.versions + ["Have_%s" % _format_version(dep.label.name)] 138 | link_flags.extend(dep.link_flags) 139 | imports += [_build_import(dep.label, im) for im in dep.imports] 140 | 141 | elif hasattr(dep, "d_srcs"): 142 | # The dependency is a d_source_library. 143 | d_srcs += dep.d_srcs 144 | transitive_d_srcs.append(dep.transitive_d_srcs) 145 | transitive_libs.append(dep.transitive_libs) 146 | link_flags += ["-L%s" % linkopt for linkopt in dep.linkopts] 147 | imports += [_build_import(dep.label, im) for im in dep.imports] 148 | versions += dep.versions 149 | 150 | elif CcInfo in dep: 151 | # The dependency is a cc_library 152 | native_libs = _a_filetype(ctx, _get_libs_for_static_executable(dep)) 153 | libs.extend(native_libs) 154 | transitive_libs.append(depset(native_libs)) 155 | 156 | else: 157 | fail("D targets can only depend on d_library, d_source_library, or " + 158 | "cc_library targets.", "deps") 159 | 160 | return struct( 161 | libs = depset(libs), 162 | transitive_libs = depset(transitive = transitive_libs), 163 | d_srcs = depset(d_srcs).to_list(), 164 | transitive_d_srcs = depset(transitive = transitive_d_srcs), 165 | versions = versions, 166 | imports = depset(imports).to_list(), 167 | link_flags = depset(link_flags).to_list(), 168 | ) 169 | 170 | def _d_library_impl(ctx): 171 | """Implementation of the d_library rule.""" 172 | d_lib = ctx.actions.declare_file((ctx.label.name + ".lib") if _is_windows(ctx) else ("lib" + ctx.label.name + ".a")) 173 | 174 | # Dependencies 175 | depinfo = _setup_deps(ctx, ctx.attr.deps, ctx.label.name, d_lib.dirname) 176 | 177 | # TODO(klknn): Combine these transitive fields into struct. 178 | trans_imports = depset(depinfo.imports, transitive = [d.trans_imports for d in ctx.attr.deps]) 179 | trans_d_srcs = depset(depinfo.d_srcs, transitive = [d.trans_d_srcs for d in ctx.attr.deps]) 180 | trans_string_imports = depset(ctx.files.string_imports, transitive = [d.trans_string_imports for d in ctx.attr.deps]) 181 | 182 | # Build compile command. 183 | compile_args = _build_compile_arglist( 184 | ctx = ctx, 185 | out = d_lib, 186 | depinfo = depinfo, 187 | imports = trans_imports.to_list(), 188 | string_imports = trans_string_imports.to_list(), 189 | extra_flags = ["-lib"], 190 | ) 191 | 192 | # Convert sources to args 193 | # This is done to support receiving a File that is a directory, as 194 | # args will auto-expand this to the contained files 195 | args = ctx.actions.args() 196 | args.add_all(compile_args) 197 | args.add_all(ctx.files.srcs) 198 | 199 | toolchain = d_toolchain(ctx) 200 | compile_inputs = depset( 201 | ctx.files.srcs + 202 | trans_string_imports.to_list() + 203 | depinfo.d_srcs + 204 | toolchain.stdlib + 205 | toolchain.stdlib_src + 206 | trans_d_srcs.to_list() + 207 | toolchain.runtime_import_src, 208 | transitive = [ 209 | depinfo.transitive_d_srcs, 210 | depinfo.libs, 211 | depinfo.transitive_libs, 212 | ], 213 | ) 214 | 215 | ctx.actions.run( 216 | inputs = compile_inputs, 217 | tools = [toolchain.compiler], 218 | outputs = [d_lib], 219 | mnemonic = "Dcompile", 220 | executable = toolchain.compiler.path, 221 | arguments = [args], 222 | use_default_shell_env = True, 223 | progress_message = "Compiling D library " + ctx.label.name, 224 | ) 225 | 226 | return struct( 227 | files = depset([d_lib]), 228 | d_srcs = ctx.files.srcs, 229 | transitive_d_srcs = depset(depinfo.d_srcs), 230 | trans_d_srcs = trans_d_srcs, 231 | transitive_libs = depset(transitive = [depinfo.libs, depinfo.transitive_libs]), 232 | link_flags = depinfo.link_flags, 233 | versions = ctx.attr.versions, 234 | imports = ctx.attr.imports, 235 | trans_imports = trans_imports, 236 | trans_string_imports = trans_string_imports, 237 | d_lib = d_lib, 238 | deps = ctx.attr.deps, 239 | ) 240 | 241 | def _d_binary_impl_common(ctx, extra_flags = []): 242 | """Common implementation for rules that build a D binary.""" 243 | d_bin = ctx.actions.declare_file(ctx.label.name + ".exe" if _is_windows(ctx) else ctx.label.name) 244 | d_obj = ctx.actions.declare_file(ctx.label.name + (".obj" if _is_windows(ctx) else ".o")) 245 | depinfo = _setup_deps(ctx, ctx.attr.deps, ctx.label.name, d_bin.dirname) 246 | trans_imports = depset(depinfo.imports, transitive = [d.trans_imports for d in ctx.attr.deps]) 247 | trans_d_srcs = depset(depinfo.d_srcs, transitive = [d.trans_d_srcs for d in ctx.attr.deps]) 248 | trans_string_imports = depset(ctx.files.string_imports, transitive = [d.trans_string_imports for d in ctx.attr.deps]) 249 | 250 | # Build compile command 251 | compile_args = _build_compile_arglist( 252 | ctx = ctx, 253 | depinfo = depinfo, 254 | out = d_obj, 255 | imports = trans_imports.to_list(), 256 | string_imports = trans_string_imports.to_list(), 257 | extra_flags = ["-c"] + extra_flags, 258 | ) 259 | 260 | # Convert sources to args 261 | # This is done to support receiving a File that is a directory, as 262 | # args will auto-expand this to the contained files 263 | args = ctx.actions.args() 264 | args.add_all(compile_args) 265 | args.add_all(ctx.files.srcs) 266 | 267 | toolchain = d_toolchain(ctx) 268 | toolchain_files = ( 269 | toolchain.stdlib + 270 | toolchain.stdlib_src + 271 | toolchain.runtime_import_src 272 | ) 273 | 274 | compile_inputs = depset( 275 | ctx.files.srcs + 276 | trans_d_srcs.to_list() + 277 | trans_string_imports.to_list() + 278 | depinfo.d_srcs + 279 | toolchain_files, 280 | transitive = [depinfo.transitive_d_srcs], 281 | ) 282 | ctx.actions.run( 283 | inputs = compile_inputs, 284 | tools = [toolchain.compiler], 285 | outputs = [d_obj], 286 | mnemonic = "Dcompile", 287 | executable = toolchain.compiler.path, 288 | arguments = [args], 289 | use_default_shell_env = True, 290 | progress_message = "Compiling D binary " + ctx.label.name, 291 | ) 292 | 293 | # Build link command 294 | link_args = _build_link_arglist( 295 | ctx = ctx, 296 | objs = [d_obj.path], 297 | depinfo = depinfo, 298 | out = d_bin, 299 | ) 300 | 301 | link_inputs = depset( 302 | [d_obj] + toolchain_files, 303 | transitive = [depinfo.libs, depinfo.transitive_libs], 304 | ) 305 | 306 | ctx.actions.run( 307 | inputs = link_inputs, 308 | tools = [toolchain.compiler], 309 | outputs = [d_bin], 310 | mnemonic = "Dlink", 311 | executable = toolchain.compiler.path, 312 | arguments = link_args, 313 | use_default_shell_env = True, 314 | progress_message = "Linking D binary " + ctx.label.name, 315 | ) 316 | 317 | return struct( 318 | d_srcs = ctx.files.srcs, 319 | transitive_d_srcs = depset(depinfo.d_srcs), 320 | imports = ctx.attr.imports, 321 | executable = d_bin, 322 | trans_imports = trans_imports, 323 | trans_d_srcs = trans_d_srcs, 324 | trans_string_imports = trans_string_imports, 325 | ) 326 | 327 | def _d_binary_impl(ctx): 328 | """Implementation of the d_binary rule.""" 329 | return _d_binary_impl_common(ctx) 330 | 331 | def _d_test_impl(ctx): 332 | """Implementation of the d_test rule.""" 333 | # TODO(klknn): Find cov files. 334 | return _d_binary_impl_common(ctx, extra_flags = ["-unittest", "-cov", "-main"]) 335 | 336 | def _get_libs_for_static_executable(dep): 337 | """ 338 | Finds the libraries used for linking an executable statically. 339 | This replaces the old API dep.cc.libs 340 | Args: 341 | dep: Target 342 | Returns: 343 | A list of File instances, these are the libraries used for linking. 344 | """ 345 | libs = [] 346 | for li in dep[CcInfo].linking_context.linker_inputs.to_list(): 347 | for library_to_link in li.libraries: 348 | if library_to_link.static_library != None: 349 | libs.append(library_to_link.static_library) 350 | elif library_to_link.pic_static_library != None: 351 | libs.append(library_to_link.pic_static_library) 352 | elif library_to_link.interface_library != None: 353 | libs.append(library_to_link.interface_library) 354 | elif library_to_link.dynamic_library != None: 355 | libs.append(library_to_link.dynamic_library) 356 | return libs 357 | 358 | def _d_source_library_impl(ctx): 359 | """Implementation of the d_source_library rule.""" 360 | transitive_d_srcs = [] 361 | transitive_libs = [] 362 | transitive_transitive_libs = [] 363 | transitive_imports = depset() 364 | transitive_linkopts = depset() 365 | transitive_versions = depset() 366 | for dep in ctx.attr.deps: 367 | if hasattr(dep, "d_srcs"): 368 | # Dependency is another d_source_library target. 369 | transitive_d_srcs.append(dep.d_srcs) 370 | transitive_imports = depset(dep.imports, transitive = [transitive_imports]) 371 | transitive_linkopts = depset(dep.linkopts, transitive = [transitive_linkopts]) 372 | transitive_versions = depset(dep.versions, transitive = [transitive_versions]) 373 | transitive_transitive_libs.append(dep.transitive_libs) 374 | 375 | elif CcInfo in dep: 376 | # Dependency is a cc_library target. 377 | native_libs = _a_filetype(ctx, _get_libs_for_static_executable(dep)) 378 | transitive_libs.extend(native_libs) 379 | 380 | else: 381 | fail("d_source_library can only depend on other " + 382 | "d_source_library or cc_library targets.", "deps") 383 | 384 | return struct( 385 | d_srcs = ctx.files.srcs, 386 | transitive_d_srcs = depset(transitive = transitive_d_srcs, order = "postorder"), 387 | transitive_libs = depset(transitive_libs, transitive = transitive_transitive_libs), 388 | imports = ctx.attr.imports + transitive_imports.to_list(), 389 | linkopts = ctx.attr.linkopts + transitive_linkopts.to_list(), 390 | versions = ctx.attr.versions + transitive_versions.to_list(), 391 | ) 392 | 393 | _d_public_attrs = { 394 | "srcs": attr.label_list(allow_files = [".d", ".di"]), 395 | "imports": attr.string_list(), 396 | "string_imports": attr.label_list(), 397 | "linkopts": attr.string_list(), 398 | "versions": attr.string_list(), 399 | "deps": attr.label_list(), 400 | } 401 | 402 | _d_common_attrs = dict(_d_public_attrs.items() + d_toolchain_attrs.items()) 403 | 404 | d_library = rule( 405 | _d_library_impl, 406 | attrs = _d_common_attrs, 407 | ) 408 | 409 | d_source_library = rule( 410 | _d_source_library_impl, 411 | attrs = _d_common_attrs, 412 | ) 413 | 414 | d_binary = rule( 415 | _d_binary_impl, 416 | attrs = _d_common_attrs, 417 | executable = True, 418 | ) 419 | 420 | d_test = rule( 421 | _d_test_impl, 422 | attrs = _d_common_attrs, 423 | executable = True, 424 | test = True, 425 | ) 426 | 427 | def d_library_with_test(name, size = "small", timeout = "short", **kwargs): 428 | d_library(name = name, **kwargs) 429 | d_test(name = name + "_test", size = size, timeout = timeout, **kwargs) 430 | -------------------------------------------------------------------------------- /source/kdr/envtool/gui.d: -------------------------------------------------------------------------------- 1 | module kdr.envtool.gui; 2 | 3 | import std.algorithm.comparison : clamp; 4 | import std.math : isClose; 5 | 6 | import dplug.core : mallocNew; 7 | import dplug.client; 8 | import dplug.math : vec2f, box2f, box2i, rectangle; 9 | import dplug.gui; 10 | // : Click, flagRaw, flagAnimated, Font, makeSizeConstraintsFixed, 11 | // makeSizeConstraintsDiscrete, MouseState, GUIGraphics, UIContext, UIElement; 12 | import dplug.graphics : cropImageRef, ImageRef, RGBA; 13 | import dplug.canvas : Canvas; 14 | import dplug.flatwidgets : UIWindowResizer; 15 | import dplug.pbrwidgets : UILabel, UIKnob; // : PBRBackgroundGUI; 16 | 17 | import kdr.envelope : Envelope; 18 | import kdr.envtool.params; 19 | import kdr.filter : filterNames; 20 | import kdr.simplegui : PBRSimpleGUI; 21 | import kdr.logging : logDebug, logInfo; 22 | 23 | enum RGBA lineColor = RGBA(0, 255, 255, 96); 24 | enum RGBA gradColor = RGBA(0, 64, 64, 96); 25 | enum RGBA gridColor = RGBA(100, 200, 200, 32); 26 | enum RGBA darkColor = RGBA(128, 128, 128, 128); 27 | enum RGBA lightColor = RGBA(100, 200, 200, 100); 28 | enum RGBA textColor = RGBA(155, 255, 255, 0); 29 | enum RGBA knobColor = RGBA(96, 96, 96, 96); 30 | enum RGBA litColor = RGBA(155, 255, 255, 0); 31 | enum RGBA unlitColor = RGBA(0, 32, 32, 0); 32 | 33 | /// UI for displaying/tweaking kdr.envelope.Envelope. 34 | class EnvelopeUI : UIElement, IParameterListener { 35 | public: 36 | @nogc nothrow: 37 | 38 | /// Ctor. 39 | this(UIContext context, Parameter[] params) { 40 | logDebug("Initialize %s", __FUNCTION__.ptr); 41 | super(context, flagRaw | flagAnimated); 42 | _params = params; 43 | foreach (Parameter p; _params) p.addListener(this); 44 | } 45 | 46 | /// Dtor. 47 | ~this() { foreach (Parameter p; _params) p.removeListener(this); } 48 | 49 | override Click onMouseClick( 50 | int x, int y, int button, bool isDoubleClick, MouseState mstate) { 51 | // Initiate drag 52 | setDirtyWhole(); 53 | 54 | _dragPoint = -1; 55 | float bias = envelopeBiasParam(_params).value; 56 | foreach (i; 0 .. Envelope.MAX_POINTS) { 57 | vec2f centerPoint; 58 | if (i == 0 || i + 1 == Envelope.MAX_POINTS) { 59 | centerPoint = vec2f(i == 0 ? 0 : 1, bias); 60 | } else { 61 | EnvelopePointParams point = envelopePointParamsAt(i, _params); 62 | if (!point.enabled.value) continue; 63 | centerPoint = vec2f(point.x.value, point.y.value); 64 | } 65 | 66 | vec2f center = point2position(centerPoint); 67 | box2f circleBox = box2f(center - pointRadius, center + pointRadius); 68 | if (circleBox.contains(x, y)) { 69 | _dragPoint = cast(int) i; 70 | logDebug("clicked %d-th point", _dragPoint); 71 | break; 72 | } 73 | } 74 | 75 | if (isDoubleClick) { 76 | if (_dragPoint != -1 && _dragPoint != 0 && _dragPoint + 1!= Envelope.MAX_POINTS) { 77 | // Found but not begin/end. 78 | EnvelopePointParams point = envelopePointParamsAt(_dragPoint, _params); 79 | if (point.curve.value) { 80 | point.enabled.beginParamEdit(); 81 | point.enabled.setFromGUI(false); 82 | point.enabled.endParamEdit(); 83 | } else { 84 | point.curve.beginParamEdit(); 85 | point.curve.setFromGUI(true); 86 | point.curve.endParamEdit(); 87 | } 88 | } else { 89 | // If not found, add a new point. 90 | foreach (i; 1 .. Envelope.MAX_POINTS - 1) { 91 | EnvelopePointParams point = envelopePointParamsAt(i, _params); 92 | // Find a disabled point. 93 | if (!point.enabled.value) { 94 | // Add the position. 95 | vec2f p = position2point(x, y); 96 | point.enabled.beginParamEdit(); 97 | point.enabled.setFromGUI(true); 98 | point.enabled.endParamEdit(); 99 | 100 | point.x.beginParamEdit(); 101 | point.x.setFromGUI(p.x); 102 | point.x.endParamEdit(); 103 | 104 | point.y.beginParamEdit(); 105 | point.y.setFromGUI(p.y); 106 | point.y.endParamEdit(); 107 | 108 | point.curve.beginParamEdit(); 109 | point.curve.setFromGUI(false); 110 | point.curve.endParamEdit(); 111 | 112 | break; 113 | } 114 | } 115 | } 116 | _dragPoint = -1; 117 | return Click.handled; 118 | } 119 | 120 | return Click.startDrag; 121 | } 122 | 123 | override void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate) { 124 | if (_dragPoint == -1) return; // But no point is selected. 125 | 126 | vec2f newp = position2point(x, y); // is already clamped to [0, 1]. 127 | 128 | if (_dragPoint == 0 || _dragPoint + 1 == Envelope.MAX_POINTS) { 129 | // As bias, only y is changed. 130 | LinearFloatParameter bias = envelopeBiasParam(_params); 131 | bias.beginParamEdit(); 132 | bias.setFromGUI(newp.y); 133 | bias.endParamEdit(); 134 | } else { 135 | // Clamp not to exceed neighbours. 136 | EnvelopePointParams point = envelopePointParamsAt(_dragPoint, _params); 137 | const float srcx = point.x.value; 138 | float prev = 0, next = 1; 139 | foreach (i; 1 .. Envelope.MAX_POINTS - 1) { 140 | const float px = envelopePointParamsAt(i, _params).x.value; 141 | if (prev < px && px < srcx) prev = px; 142 | if (srcx < px && px < next) next = px; 143 | } 144 | newp.x = clamp(newp.x, prev, next); 145 | 146 | point.x.beginParamEdit(); 147 | point.x.setFromGUI(newp.x); 148 | point.x.endParamEdit(); 149 | 150 | point.y.beginParamEdit(); 151 | point.y.setFromGUI(newp.y); 152 | point.y.endParamEdit(); 153 | } 154 | 155 | setDirtyWhole(); 156 | } 157 | 158 | override void onAnimate(double dt, double time) nothrow @nogc { 159 | if (_timeDisplayError > 0.0f) { 160 | _timeDisplayError = _timeDisplayError - dt; 161 | if (_timeDisplayError < 0) _timeDisplayError = 0; 162 | setDirtyWhole(); 163 | } 164 | } 165 | 166 | override void onDrawRaw(ImageRef!RGBA rawMap, box2i[] dirtyRects) { 167 | Envelope env = buildEnvelope(_params); 168 | foreach (ref rect; dirtyRects) { 169 | ImageRef!RGBA cropped = cropImageRef(rawMap, rect); 170 | _canvas.initialize(cropped); 171 | _canvas.translate(-rect.min.x, -rect.min.y); 172 | 173 | // Draw background. 174 | _canvas.fillStyle = RGBA(0, 32, 32, 200); 175 | _canvas.fillRect(0, 0, position.width, position.height); 176 | 177 | // Draw grid. 178 | enum float gridWidth = 0.0015; 179 | int numGrid = 8; 180 | foreach (float i; 0 .. numGrid + 1) { 181 | _canvas.fillStyle = gridColor; 182 | _canvas.beginPath(); 183 | _canvas.moveTo(point2position(vec2f(i / numGrid - gridWidth, 0))); 184 | _canvas.lineTo(point2position(vec2f(i / numGrid + gridWidth, 0))); 185 | _canvas.lineTo(point2position(vec2f(i / numGrid + gridWidth, 1))); 186 | _canvas.lineTo(point2position(vec2f(i / numGrid - gridWidth, 1))); 187 | _canvas.fill(); 188 | 189 | _canvas.beginPath(); 190 | _canvas.moveTo(point2position(vec2f(0, i / numGrid - gridWidth))); 191 | _canvas.lineTo(point2position(vec2f(0, i / numGrid + gridWidth))); 192 | _canvas.lineTo(point2position(vec2f(1, i / numGrid + gridWidth))); 193 | _canvas.lineTo(point2position(vec2f(1, i / numGrid - gridWidth))); 194 | _canvas.fill(); 195 | } 196 | 197 | // Draw points. 198 | foreach (Envelope.Point p; env) { 199 | _canvas.fillStyle = p.isCurve ? darkColor : lightColor; 200 | _canvas.fillCircle(point2position(p), pointRadius); 201 | } 202 | 203 | // Draw envelope lines. 204 | auto grad = _canvas.createLinearGradient(0, 0, 0, position.height); 205 | grad.addColorStop(0, lineColor); 206 | grad.addColorStop(position.height, gradColor); 207 | _canvas.fillStyle = grad; 208 | _canvas.beginPath(); 209 | _canvas.moveTo(point2position(vec2f(0, 0))); 210 | enum numLine = 1000; 211 | foreach (float n; 0 .. numLine) { 212 | const float x = n / numLine; 213 | const float y = env.getY(x); 214 | _canvas.lineTo(point2position(vec2f(x, y))); 215 | } 216 | _canvas.lineTo(point2position(vec2f(1, 0))); 217 | _canvas.fill(); 218 | } 219 | } 220 | 221 | // Account for param changes. 222 | 223 | override void onParameterChanged(Parameter sender) { 224 | setDirtyWhole(); 225 | } 226 | 227 | override void onBeginParameterEdit(Parameter sender) {} 228 | 229 | override void onEndParameterEdit(Parameter sender) {} 230 | 231 | override void onBeginParameterHover(Parameter sender) {} 232 | 233 | override void onEndParameterHover(Parameter sender) {} 234 | 235 | private: 236 | float pointRadius() { return position.width / 50; } 237 | 238 | // Converts point in [0, 1] to dirty rect position in UI. 239 | vec2f point2position(vec2f p) { 240 | const float w = position.width - pointRadius * 2; 241 | const float h = position.height- pointRadius * 2; 242 | return vec2f(w * p.x + pointRadius, h - h * p.y + pointRadius); 243 | } 244 | 245 | vec2f position2point(float[2] pos ...) { 246 | const float w = position.width - pointRadius * 2; 247 | const float h = position.height- pointRadius * 2; 248 | return vec2f(clamp((pos[0] - pointRadius) / w, 0, 1), 249 | clamp((h - pos[1] + pointRadius) / h, 0, 1)); 250 | } 251 | 252 | // States. 253 | Canvas _canvas; 254 | float _timeDisplayError = 0; 255 | int _dragPoint = -1; 256 | Parameter[] _params; 257 | } 258 | 259 | unittest { 260 | GUIGraphics gui = new GUIGraphics(makeSizeConstraintsFixed(200, 100), 261 | flagRaw | flagAnimated); 262 | EnvelopeUI ui = new EnvelopeUI(gui.context, []); 263 | ui.position = rectangle(0, 0, 200, 100); 264 | 265 | vec2f pos = ui.point2position(vec2f(0.1, 0.2)); 266 | vec2f point = ui.position2point(pos.x, pos.y); 267 | assert(isClose(point.x, 0.1)); 268 | assert(isClose(point.y, 0.2)); 269 | } 270 | 271 | /// 272 | class EnvToolGUI : PBRSimpleGUI, IParameterListener { 273 | public: 274 | @nogc nothrow: 275 | this(Parameter[] params) { 276 | logDebug("Initialize %s", __FUNCTION__.ptr); 277 | 278 | static immutable float[] ratios = [1.0f, 1.25f, 1.5f, 1.75f, 2.0f]; 279 | super(makeSizeConstraintsDiscrete(600, 400, ratios)); 280 | 281 | _params = params; 282 | _font = mallocNew!Font(cast(ubyte[]) import("FORCED SQUARE.ttf")); 283 | 284 | _title = buildLabel("kdr envtool"); 285 | _date = buildLabel("build: " ~ __DATE__ ~ ""); 286 | 287 | addChild(_resizer = mallocNew!UIWindowResizer(context())); 288 | addChild(_envui = mallocNew!EnvelopeUI(context(), params)); 289 | 290 | // General knobs and labels. 291 | _rateKnob = buildKnob(Params.rate); 292 | _rateLabel = buildLabel("rate"); 293 | _params[Params.rate].addListener(this); 294 | 295 | _depthKnob = buildKnob(Params.depth); 296 | _depthLabel = buildLabel("depth"); 297 | 298 | _stereoOffsetKnob = buildKnob(Params.stereoOffset); 299 | _stereoOffsetLabel = buildLabel("offset"); 300 | 301 | _destinationKnob = buildKnob(Params.destination); 302 | _destinationLabel = buildLabel("dst"); 303 | _params[Params.destination].addListener(this); 304 | 305 | // Filter knobs and labels. 306 | _filterKindKnob = buildKnob(Params.filterKind); 307 | _filterKindLabel = buildLabel("filter"); 308 | _params[Params.filterKind].addListener(this); 309 | 310 | _filterCutoffKnob = buildKnob(Params.filterCutoff); 311 | _filterCutoffLabel = buildLabel("cutoff"); 312 | 313 | _filterResKnob = buildKnob(Params.filterRes); 314 | _filterResLabel = buildLabel("q"); 315 | } 316 | 317 | ~this() { 318 | destroyFree(_font); 319 | } 320 | 321 | override void reflow() { 322 | super.reflow(); 323 | const int W = position.width; 324 | const int H = position.height; 325 | 326 | // Main. 70% 327 | _envui.position = rectangle(0, 0, cast(int) (W * 0.7), cast(int) (H * 0.9)); 328 | 329 | // Knobs. 40% 330 | float knobAndLabel = H / 4.0; 331 | int knobSize = cast(int) (knobAndLabel * 0.8); 332 | int knobX = cast(int) (W * 0.725); 333 | int labelSize = cast(int) (knobAndLabel * 0.17); 334 | int labelMargin = cast(int) (knobAndLabel * 0.03); 335 | 336 | _rateKnob.position = rectangle( 337 | knobX, labelMargin, knobSize, knobSize); 338 | _rateLabel.textSize = labelSize; 339 | _rateLabel.position = rectangle( 340 | knobX, _rateKnob.position.max.y, knobSize, labelSize); 341 | 342 | _depthKnob.position = rectangle( 343 | knobX, _rateLabel.position.max.y, knobSize, knobSize); 344 | _depthLabel.textSize = labelSize; 345 | _depthLabel.position = rectangle( 346 | knobX, _depthKnob.position.max.y, knobSize, labelSize); 347 | 348 | _stereoOffsetKnob.position = rectangle( 349 | knobX, _depthLabel.position.max.y, knobSize, knobSize); 350 | _stereoOffsetLabel.textSize = labelSize; 351 | _stereoOffsetLabel.position = rectangle( 352 | knobX, _stereoOffsetKnob.position.max.y, knobSize, labelSize); 353 | 354 | _destinationKnob.position = rectangle( 355 | knobX, _stereoOffsetLabel.position.max.y, knobSize, knobSize); 356 | _destinationLabel.position = rectangle( 357 | knobX, _destinationKnob.position.max.y, knobSize, labelSize); 358 | _destinationLabel.textSize = labelSize; 359 | 360 | // filter. 361 | knobX += knobSize; 362 | _filterKindKnob.position = rectangle( 363 | knobX, labelMargin, knobSize, knobSize); 364 | _filterKindLabel.textSize = labelSize; 365 | _filterKindLabel.position = rectangle( 366 | knobX, _filterKindKnob.position.max.y, knobSize, labelSize); 367 | 368 | _filterCutoffKnob.position = rectangle( 369 | knobX, _filterKindLabel.position.max.y, knobSize, knobSize); 370 | _filterCutoffLabel.textSize = labelSize; 371 | _filterCutoffLabel.position = rectangle( 372 | knobX, _filterCutoffKnob.position.max.y, knobSize, labelSize); 373 | 374 | _filterResKnob.position = rectangle( 375 | knobX, _filterCutoffLabel.position.max.y, knobSize, knobSize); 376 | _filterResLabel.textSize = labelSize; 377 | _filterResLabel.position = rectangle( 378 | knobX, _filterResKnob.position.max.y, knobSize, labelSize); 379 | 380 | // etc. 381 | _title.position = rectangle(0, _envui.position.max.y, 382 | _envui.position.width * 2 / 3, 383 | cast(int) (H * 0.1)); 384 | _title.textSize = _title.position.height; 385 | 386 | int dateLabelSize = cast(int) _title.textSize / 3; 387 | _date.position = rectangle(_title.position.max.x, 388 | H - dateLabelSize, 389 | _envui.position.width - _title.position.width, 390 | dateLabelSize); 391 | _date.textSize = dateLabelSize; 392 | 393 | int hintSize = H / 20; 394 | _resizer.position = rectangle(W - hintSize, H - hintSize, 395 | hintSize, hintSize); 396 | } 397 | 398 | override void onParameterChanged(Parameter sender) { 399 | if (sender.index == Params.rate) { 400 | if (EnumParameter rate = cast(EnumParameter) sender) { 401 | _rateLabel.text(rateLabels[rate.value]); 402 | } 403 | } else if (sender.index == Params.destination) { 404 | if (EnumParameter dest = cast(EnumParameter) sender) { 405 | _destinationLabel.text(destinationNames[dest.value]); 406 | } 407 | } else if (sender.index == Params.filterKind) { 408 | if (EnumParameter kind = cast(EnumParameter) sender) { 409 | _filterKindLabel.text(filterNames[kind.value]); 410 | } 411 | } 412 | } 413 | 414 | override void onBeginParameterEdit(Parameter sender) {} 415 | 416 | override void onEndParameterEdit(Parameter sender) {} 417 | 418 | override void onBeginParameterHover(Parameter sender) {} 419 | 420 | override void onEndParameterHover(Parameter sender) {} 421 | 422 | private: 423 | UIKnob buildKnob(Params pid) { 424 | UIKnob knob; 425 | addChild(knob = mallocNew!UIKnob(this.context, _params[pid])); 426 | knob.knobRadius = 0.65f; 427 | knob.knobDiffuse = knobColor; 428 | // NOTE: material [R(smooth), G(metal), B(shiny), A(phisycal)] 429 | knob.knobMaterial = RGBA(255, 0, 0, 0); 430 | knob.numLEDs = 0; 431 | knob.litTrailDiffuse = litColor; 432 | knob.unlitTrailDiffuse = unlitColor; 433 | knob.trailRadiusMin = 0.1; 434 | knob.trailRadiusMax = 0.8; 435 | return knob; 436 | } 437 | 438 | UILabel buildLabel(string text) { 439 | UILabel label; 440 | addChild(label = mallocNew!UILabel(this.context, _font, text)); 441 | label.textColor = textColor; 442 | return label; 443 | } 444 | 445 | Font _font; 446 | UILabel _title, _date; 447 | Parameter[] _params; 448 | UIWindowResizer _resizer; 449 | EnvelopeUI _envui; 450 | UIKnob _rateKnob, _depthKnob, _stereoOffsetKnob, _destinationKnob, 451 | _filterKindKnob, _filterCutoffKnob, _filterResKnob; 452 | UILabel _rateLabel, _depthLabel, _stereoOffsetLabel, _destinationLabel, 453 | _filterKindLabel, _filterCutoffLabel, _filterResLabel; 454 | 455 | enum litTrailDiffuse = RGBA(151, 119, 255, 100); 456 | enum unlitTrailDiffuse = RGBA(81, 54, 108, 0); 457 | } 458 | 459 | unittest { 460 | Parameter[] ps = buildEnvelopeParameters(); 461 | auto gui = new EnvToolGUI(ps); 462 | gui.reflow(); 463 | 464 | int w = 100, h = 100; 465 | auto dif = new OwnedImage!RGBA(w, h); 466 | auto dep = new OwnedImage!L16(w, h); 467 | auto mat = new OwnedImage!RGBA(w, h); 468 | gui.onDrawPBR(toRef(dif), toRef(dep), toRef(mat), [rectangle(0, 0, w, h)]); 469 | } 470 | -------------------------------------------------------------------------------- /source/kdr/epiano2/client.d: -------------------------------------------------------------------------------- 1 | module kdr.epiano2.client; 2 | 3 | import std.algorithm : min; 4 | 5 | import dplug.core : mallocNew, makeVec, Vec; 6 | import dplug.client : Client, IntegerParameter, LegalIO, LinearFloatParameter, MidiControlChange, MidiMessage, Parameter, PluginInfo, Preset, TimeInfo; 7 | import mir.math : fabs, fastmath, exp, pow; 8 | 9 | import kdr.audiofmt : Wav; 10 | import kdr.epiano2.parameter : ModParameter; 11 | import kdr.testing : benchmarkWithDefaultParams; 12 | 13 | enum Param : int { 14 | envelopeDecay = 0, 15 | envelopeRelease = 1, 16 | hardness = 2, 17 | trebleBoost = 3, 18 | modulation = 4, 19 | lfoRate = 5, // see resume() to get unnormalized val 20 | velocitySense = 6, 21 | stereoWidth = 7, 22 | polyphony = 8, 23 | fineTuning = 9, 24 | randomTuning = 10, 25 | overdrive = 11, 26 | } 27 | 28 | struct Voice { 29 | // Sample playback. 30 | int delta; 31 | int frac; 32 | int pos; 33 | int end; 34 | int loop; 35 | 36 | // Envelope. 37 | float env = 0.0f; 38 | float dec = 0.99f; // all notes off. 39 | 40 | // First-order LPF. 41 | float f0 = 0; 42 | float f1 = 0; 43 | float ff = 0; 44 | 45 | float outl = 0; 46 | float outr = 0; 47 | int note; 48 | } 49 | 50 | struct KeyGroup { 51 | int root; 52 | int high; 53 | int pos; 54 | int end; 55 | int loop; 56 | } 57 | 58 | class Epiano2Client : Client { 59 | @fastmath nothrow @nogc public: 60 | 61 | this() { 62 | super(); 63 | //Waveform data and keymapping 64 | kgrp[ 0].root = 36; kgrp[ 0].high = 39; //C1 65 | kgrp[ 3].root = 43; kgrp[ 3].high = 45; //G1 66 | kgrp[ 6].root = 48; kgrp[ 6].high = 51; //C2 67 | kgrp[ 9].root = 55; kgrp[ 9].high = 57; //G2 68 | kgrp[12].root = 60; kgrp[12].high = 63; //C3 69 | kgrp[15].root = 67; kgrp[15].high = 69; //G3 70 | kgrp[18].root = 72; kgrp[18].high = 75; //C4 71 | kgrp[21].root = 79; kgrp[21].high = 81; //G4 72 | kgrp[24].root = 84; kgrp[24].high = 87; //C5 73 | kgrp[27].root = 91; kgrp[27].high = 93; //G5 74 | kgrp[30].root = 96; kgrp[30].high =999; //C6 75 | 76 | kgrp[0].pos = 0; kgrp[0].end = 8476; kgrp[0].loop = 4400; 77 | kgrp[1].pos = 8477; kgrp[1].end = 16248; kgrp[1].loop = 4903; 78 | kgrp[2].pos = 16249; kgrp[2].end = 34565; kgrp[2].loop = 6398; 79 | kgrp[3].pos = 34566; kgrp[3].end = 41384; kgrp[3].loop = 3938; 80 | kgrp[4].pos = 41385; kgrp[4].end = 45760; kgrp[4].loop = 1633; //was 1636; 81 | kgrp[5].pos = 45761; kgrp[5].end = 65211; kgrp[5].loop = 5245; 82 | kgrp[6].pos = 65212; kgrp[6].end = 72897; kgrp[6].loop = 2937; 83 | kgrp[7].pos = 72898; kgrp[7].end = 78626; kgrp[7].loop = 2203; //was 2204; 84 | kgrp[8].pos = 78627; kgrp[8].end = 100387; kgrp[8].loop = 6368; 85 | kgrp[9].pos = 100388; kgrp[9].end = 116297; kgrp[9].loop = 10452; 86 | kgrp[10].pos = 116298; kgrp[10].end = 127661; kgrp[10].loop = 5217; //was 5220; 87 | kgrp[11].pos = 127662; kgrp[11].end = 144113; kgrp[11].loop = 3099; 88 | kgrp[12].pos = 144114; kgrp[12].end = 152863; kgrp[12].loop = 4284; 89 | kgrp[13].pos = 152864; kgrp[13].end = 173107; kgrp[13].loop = 3916; 90 | kgrp[14].pos = 173108; kgrp[14].end = 192734; kgrp[14].loop = 2937; 91 | kgrp[15].pos = 192735; kgrp[15].end = 204598; kgrp[15].loop = 4732; 92 | kgrp[16].pos = 204599; kgrp[16].end = 218995; kgrp[16].loop = 4733; 93 | kgrp[17].pos = 218996; kgrp[17].end = 233801; kgrp[17].loop = 2285; 94 | kgrp[18].pos = 233802; kgrp[18].end = 248011; kgrp[18].loop = 4098; 95 | kgrp[19].pos = 248012; kgrp[19].end = 265287; kgrp[19].loop = 4099; 96 | kgrp[20].pos = 265288; kgrp[20].end = 282255; kgrp[20].loop = 3609; 97 | kgrp[21].pos = 282256; kgrp[21].end = 293776; kgrp[21].loop = 2446; 98 | kgrp[22].pos = 293777; kgrp[22].end = 312566; kgrp[22].loop = 6278; 99 | kgrp[23].pos = 312567; kgrp[23].end = 330200; kgrp[23].loop = 2283; 100 | kgrp[24].pos = 330201; kgrp[24].end = 348889; kgrp[24].loop = 2689; 101 | kgrp[25].pos = 348890; kgrp[25].end = 365675; kgrp[25].loop = 4370; 102 | kgrp[26].pos = 365676; kgrp[26].end = 383661; kgrp[26].loop = 5225; 103 | kgrp[27].pos = 383662; kgrp[27].end = 393372; kgrp[27].loop = 2811; 104 | kgrp[28].pos = 383662; kgrp[28].end = 393372; kgrp[28].loop = 2811; //ghost 105 | kgrp[29].pos = 393373; kgrp[29].end = 406045; kgrp[29].loop = 4522; 106 | kgrp[30].pos = 406046; kgrp[30].end = 414486; kgrp[30].loop = 2306; 107 | kgrp[31].pos = 406046; kgrp[31].end = 414486; kgrp[31].loop = 2306; //ghost 108 | kgrp[32].pos = 414487; kgrp[32].end = 422408; kgrp[32].loop = 2169; 109 | 110 | 111 | Wav epianoWav = Wav(import("epiano.wav")); 112 | const short[] epianoData = epianoWav.data; 113 | waves = makeVec!short(epianoData.length); 114 | waves.ptr[0 .. epianoData.length] = epianoData; 115 | 116 | //extra xfade looping... 117 | foreach (k; 0 .. 28) { 118 | int p0 = kgrp[k].end; 119 | int p1 = kgrp[k].end - kgrp[k].loop; 120 | 121 | float xf = 1.0f; 122 | float dxf = -0.02f; 123 | 124 | while(xf > 0.0f) { 125 | waves[p0] = cast(short)((1.0f - xf) * cast(float)waves[p0] 126 | + xf * cast(float)waves[p1]); 127 | p0--; 128 | p1--; 129 | xf += dxf; 130 | } 131 | } 132 | } 133 | 134 | /// Needs to be overriden in bin/epiano2/main.d. 135 | override PluginInfo buildPluginInfo() { 136 | return PluginInfo.init; 137 | } 138 | 139 | override Preset[] buildPresets() { 140 | auto presets = makeVec!Preset(); 141 | 142 | void fillpatch(string name, float[12] params...) { 143 | presets ~= mallocNew!Preset(name, params[]); 144 | } 145 | 146 | fillpatch("Default", 0.500f, 0.500f, 0.500f, 0.500f, 0.500f, 0.650f, 0.250f, 0.500f, 0.50f, 0.500f, 0.146f, 0.000f); 147 | fillpatch("Bright", 0.500f, 0.500f, 1.000f, 0.800f, 0.500f, 0.650f, 0.250f, 0.500f, 0.50f, 0.500f, 0.146f, 0.500f); 148 | fillpatch("Mellow", 0.500f, 0.500f, 0.000f, 0.000f, 0.500f, 0.650f, 0.250f, 0.500f, 0.50f, 0.500f, 0.246f, 0.000f); 149 | fillpatch("Autopan", 0.500f, 0.500f, 0.500f, 0.500f, 0.250f, 0.650f, 0.250f, 0.500f, 0.50f, 0.500f, 0.246f, 0.000f); 150 | fillpatch("Tremolo", 0.500f, 0.500f, 0.500f, 0.500f, 0.750f, 0.650f, 0.250f, 0.500f, 0.50f, 0.500f, 0.246f, 0.000f); 151 | fillpatch("(default)", 0.500f, 0.500f, 0.500f, 0.500f, 0.500f, 0.650f, 0.250f, 0.500f, 0.50f, 0.500f, 0.146f, 0.000f); 152 | fillpatch("(default)", 0.500f, 0.500f, 0.500f, 0.500f, 0.500f, 0.650f, 0.250f, 0.500f, 0.50f, 0.500f, 0.146f, 0.000f); 153 | fillpatch("(default)", 0.500f, 0.500f, 0.500f, 0.500f, 0.500f, 0.650f, 0.250f, 0.500f, 0.50f, 0.500f, 0.146f, 0.000f); 154 | return presets.releaseData; 155 | } 156 | 157 | override Parameter[] buildParameters() const { 158 | auto params = makeVec!Parameter(); 159 | 160 | params ~= mallocNew!IntegerParameter( 161 | /*index=*/Param.envelopeDecay, /*name=*/"Envelope Decay", /*label=*/"%", 162 | /*min=*/0, /*max=*/100, /*defaultValue=*/50); 163 | 164 | params ~= mallocNew!IntegerParameter( 165 | /*index=*/Param.envelopeRelease, /*name=*/"Envelope Release", 166 | /*label=*/"%", 167 | /*min=*/0, /*max=*/100, /*defaultValue=*/50); 168 | 169 | params ~= mallocNew!IntegerParameter( 170 | /*index=*/Param.hardness, /*name=*/"Hardness", 171 | /*label=*/"%", 172 | /*min=*/-50, /*max=*/50, /*defaultValue=*/0); 173 | 174 | params ~= mallocNew!IntegerParameter( 175 | /*index=*/Param.trebleBoost, /*name=*/"Treble Boost", /*label=*/"%", 176 | /*min=*/-50, /*max=*/50, /*defaultValue=*/0); 177 | 178 | params ~= mallocNew!ModParameter( 179 | /*index=*/Param.modulation, /*name=*/"Modulation", /*label=*/"%", 180 | /*min=*/-100, /*max=*/100, /*defaultValue=*/0); 181 | 182 | params ~= mallocNew!LinearFloatParameter( 183 | /*index=*/Param.lfoRate, /*name=*/"LFO rate", "Hz", 184 | /*min=*/0.07, /*max=*/36.97, /*defaultValue=*/4.19); 185 | 186 | params ~= mallocNew!IntegerParameter( 187 | /*index=*/Param.velocitySense, /*name=*/"Velocity Sense", /*label=*/"%", 188 | /*min=*/0, /*max=*/100, /*defaultValue=*/25); 189 | 190 | params ~= mallocNew!IntegerParameter( 191 | /*index=*/Param.stereoWidth, /*name=*/"Stereo Width", /*label=*/"%", 192 | /*min=*/0, /*max=*/200, /*defaultValue=*/100); 193 | 194 | params ~= mallocNew!IntegerParameter( 195 | /*index=*/Param.polyphony, /*name=*/"Polyphony", /*label=*/"voices", 196 | /*min=*/0, /*max=*/32, /*defaultValue=*/16); 197 | 198 | params ~= mallocNew!IntegerParameter( 199 | /*index=*/Param.fineTuning, /*name=*/"Fine Tuning", 200 | /*label=*/"cents", 201 | /*min=*/-50, /*max=*/50, /*defaultValue=*/0); 202 | 203 | params ~= mallocNew!LinearFloatParameter( 204 | /*index=*/Param.randomTuning, /*name=*/"Random Tuning", "cents", 205 | /*min=*/0.0, /*max=*/50.0, /*defaultValue=*/1.1); 206 | 207 | params ~= mallocNew!LinearFloatParameter( 208 | /*index=*/Param.overdrive, /*name=*/"Overdrive", "%", 209 | /*min=*/0.0, /*max=*/100.0, /*defaultValue=*/0.0); 210 | 211 | return params.releaseData(); 212 | } 213 | 214 | override LegalIO[] buildLegalIO() const { 215 | auto io = makeVec!LegalIO(); 216 | io ~= LegalIO(/*numInputChannels=*/0, /*numOutputChannels*/2); 217 | return io.releaseData(); 218 | } 219 | 220 | override void reset( 221 | double sampleRate, int maxFrames, int numInputs, int numOutputs) { 222 | Fs = sampleRate; 223 | iFs = 1f / sampleRate; 224 | } 225 | 226 | override int maxFramesInProcess() { return 64; } 227 | 228 | override void processAudio( 229 | const(float*)[] inputs, float*[] outputs, int sampleFrames, TimeInfo info) { 230 | processParams(); 231 | processMidi(sampleFrames); 232 | 233 | int index, event, frame; 234 | while(framesampleFrames) frames = sampleFrames; 237 | frames -= frame; 238 | frame += frames; 239 | 240 | while(--frames>=0) { 241 | auto l = 0f; 242 | auto r = 0f; 243 | 244 | foreach (ref V; voice) { 245 | V.frac += V.delta; //integer-based linear interpolation 246 | V.pos += V.frac >> 16; 247 | V.frac &= 0xFFFF; 248 | if(V.pos > V.end) V.pos -= V.loop; 249 | int i = waves[V.pos]; 250 | i = (i << 7) + (V.frac >> 9) * (waves[V.pos + 1] - i) + 0x40400000; 251 | auto x = V.env * (*cast(float *)&i - 3.0f); //fast int.float 252 | V.env = V.env * V.dec; //envelope 253 | 254 | if(x>0.0f) { x -= overdrive * x * x; if(x < -V.env) x = -V.env; } //+= 0.5f * x * x; } //overdrive 255 | 256 | l += V.outl * x; 257 | r += V.outr * x; 258 | } 259 | 260 | tl += tfrq * (l - tl); //treble boost 261 | tr += tfrq * (r - tr); 262 | r += treb * (r - tr); 263 | l += treb * (l - tl); 264 | lfo0 += dlfo * lfo1; //LFO for tremolo and autopan 265 | lfo1 -= dlfo * lfo0; 266 | l += l * lmod * lfo1; 267 | r += r * rmod * lfo1; //worth making all these local variables? 268 | 269 | outputs[0][index] = l; 270 | outputs[1][index] = r; 271 | ++index; 272 | } 273 | 274 | if(frame 0.5f) { 277 | lfo0 = -0.7071f; 278 | lfo1 = 0.7071f; 279 | } 280 | int note = notes[event++]; 281 | int vel = notes[event++]; 282 | if (vel > 0) noteOn(note, vel); 283 | else noteOff(note); 284 | } 285 | } 286 | if(fabs(tl)<1.0e-10) tl = 0.0f; //anti-denormal 287 | if(fabs(tr)<1.0e-10) tr = 0.0f; 288 | 289 | for (int v=0; v 0.05f) { 313 | rmod = lmod = modwhl; //lfo depth 314 | if(modulation < 0.5f) rmod = -rmod; 315 | } 316 | break; 317 | case MidiControlChange.channelVolume: 318 | volume = 0.00002f * cast(float) (msg.controlChangeValue * msg.controlChangeValue); 319 | break; 320 | 321 | case MidiControlChange.sustainOnOff: 322 | case MidiControlChange.sustenutoOnOff: 323 | sustain = msg.controlChangeValue & 0x44; 324 | if (sustain == 0) { 325 | notes[npos++] = msg.offset; 326 | notes[npos++] = SUSTAIN; //end all sustained notes 327 | notes[npos++] = 0; 328 | } 329 | break; 330 | case MidiControlChange.allNotesOff: 331 | case MidiControlChange.allSoundsOff: 332 | for(int v=0; v EVENTBUFFER) npos -= 3; // discard events if buffer full 343 | } 344 | notes[npos] = EVENTS_DONE; 345 | } 346 | 347 | void noteOn(int note, int velocity) { 348 | int vl; 349 | if(activevoices < poly) { 350 | vl = activevoices; 351 | activevoices++; 352 | voice[vl].f0 = voice[vl].f1 = 0.0f; 353 | } else { 354 | //steal a note 355 | //find quietest voice 356 | float l = float.infinity; 357 | foreach (v; 0 .. poly) { 358 | if(voice[v].env < l) { l = voice[v].env; vl = v; } 359 | } 360 | } 361 | 362 | int k = (note - 60) * (note - 60); 363 | float l = fine + random * (cast(float)(k % 13) - 6.5f); //random & fine tune 364 | if(note > 60) l += stretch * cast(float)k; //stretch 365 | 366 | k = 0; 367 | while(note > (kgrp[k].high + size)) k += 3; //find keygroup 368 | 369 | l += cast(float)(note - kgrp[k].root); //pitch 370 | l = 32000.0f * iFs * cast(float)exp(0.05776226505 * l); 371 | voice[vl].delta = cast(int)(65536.0f * l); 372 | voice[vl].frac = 0; 373 | 374 | if(velocity > 48) k++; //mid velocity sample 375 | if(velocity > 80) k++; //high velocity sample 376 | voice[vl].pos = kgrp[k].pos; 377 | voice[vl].end = kgrp[k].end - 1; 378 | voice[vl].loop = kgrp[k].loop; 379 | 380 | voice[vl].env = (3.0f + 2.0f * velsens) * cast(float)pow(0.0078f * velocity, velsens); //velocity 381 | 382 | if(note > 60) voice[vl].env *= cast(float)exp(0.01f * cast(float)(60 - note)); //new! high notes quieter 383 | 384 | l = 50.0f + modulation * modulation * muff + muffvel * cast(float)(velocity - 64); //muffle 385 | if(l < (55.0f + 0.4f * cast(float)note)) l = 55.0f + 0.4f * cast(float)note; 386 | if(l > 210.0f) l = 210.0f; 387 | voice[vl].ff = l * l * iFs; 388 | 389 | voice[vl].note = note; //note->pan 390 | if(note < 12) note = 12; 391 | if(note > 108) note = 108; 392 | l = volume; 393 | voice[vl].outr = l + l * width * cast(float)(note - 60); 394 | voice[vl].outl = l + l - voice[vl].outr; 395 | 396 | if (note < 44) note = 44; 397 | voice[vl].dec = cast(float) exp( 398 | -iFs * exp(-1.0 + 0.03 * cast(double)note - 2.0f * decay)); 399 | } 400 | 401 | void noteOff(int note) { 402 | const dec = cast(float) exp(-iFs * exp(6.0 + 0.01 * cast(double) note - 5.0 * release)); 403 | foreach (v; 0 .. NVOICES) { 404 | //any voices playing that note? 405 | if(voice[v].note == note) { 406 | if(sustain == 0) voice[v].dec = dec; 407 | else voice[v].note = SUSTAIN; 408 | } 409 | } 410 | } 411 | 412 | void processParams() { 413 | size = cast(int) (12f * param(Param.hardness).getNormalized - 6.0); 414 | 415 | auto trebNormalized = param(Param.trebleBoost).getNormalized; 416 | treb = 4f * trebNormalized * trebNormalized - 1f; 417 | tfrq = (trebNormalized > 0.5f) ? 14000f : 5000f; 418 | tfrq = 1.0f - cast(float) exp(-iFs * tfrq); 419 | 420 | auto modNormalized = param(Param.modulation).getNormalized; 421 | rmod = lmod = 2 * modNormalized - 1.0f; //lfo depth 422 | if(modNormalized < 0.5f) rmod = -rmod; 423 | 424 | dlfo = 6.283f * iFs * cast(float)exp(6.22f * param(Param.lfoRate).getNormalized - 2.61f); //lfo rate 425 | 426 | auto velsensNormalized = param(Param.velocitySense).getNormalized; 427 | velsens = 2 * velsensNormalized + 1.0f; 428 | if(velsensNormalized < 0.25f) velsens -= 0.75f - 3.0f * velsensNormalized; 429 | 430 | width = 0.03f * param(Param.stereoWidth).getNormalized; 431 | poly = 1 + cast(int)(31.9f * param(Param.polyphony).getNormalized); 432 | fine = param(Param.fineTuning).getNormalized - 0.5f; 433 | auto randomNormalized = param(Param.randomTuning).getNormalized; 434 | random = 0.077f * randomNormalized * randomNormalized; 435 | stretch = 0.0f; //0.000434f * (param[11] - 0.5f); parameter re-used for overdrive! 436 | overdrive = 1.8f * param(Param.overdrive).getNormalized; 437 | 438 | release = param(Param.envelopeRelease).getNormalized; 439 | decay = param(Param.envelopeDecay).getNormalized; 440 | modulation = param(Param.modulation).getNormalized; 441 | } 442 | 443 | float Fs, iFs; 444 | 445 | enum EVENTBUFFER = 120; 446 | enum EVENTS_DONE = 99999999; 447 | //list of delta|note|velocity for current block 448 | int[EVENTBUFFER + 8] notes = [EVENTS_DONE]; 449 | 450 | // Global internal variables 451 | KeyGroup[34] kgrp; 452 | Vec!short waves; 453 | 454 | enum SILENCE = 0.0001f; 455 | enum SUSTAIN = 128; 456 | enum NVOICES = 32; 457 | Voice[NVOICES] voice; 458 | 459 | int activevoices = 0, poly = 16; 460 | float width = 0; 461 | int size, sustain = 0; 462 | float lfo0 = 0, lfo1 = 1, dlfo = 0, lmod = 0, rmod = 0; 463 | float treb = 0, tfrq = 0.5, tl = 0, tr = 0; 464 | float tune = 0, fine = 0, random = 0, stretch = 0, overdrive = 0; 465 | float muff = 160, muffvel = 0, sizevel, velsens = 1, volume = 0.2, modwhl = 0; 466 | float modulation = 0, decay = 0, release = 0; 467 | } 468 | 469 | unittest { 470 | benchmarkWithDefaultParams!Epiano2Client; 471 | } 472 | --------------------------------------------------------------------------------