├── .github └── workflows │ ├── build.yaml │ └── test_published_packages.yaml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── build_scripts ├── build_in_manylinux_container.sh ├── build_in_windows.sh └── build_on_host.sh ├── cython_vst_loader ├── .gitignore ├── __init__.py ├── dto │ ├── __init__.py │ └── vst_time_info.py ├── exceptions.py ├── include │ └── aeffectx_with_additional_structures.h ├── vst_constants.py ├── vst_event.py ├── vst_host.py ├── vst_loader_wrapper.pyx └── vst_plugin.py ├── doc ├── build_and_release.md ├── development.md ├── git │ └── hooks │ │ └── pre-commit-msg └── usage_examples.md ├── inspect_platform.py ├── requirements.txt ├── setup.py └── tests ├── linting_test.py ├── test_buffers.py ├── test_plugins ├── .gitignore ├── DragonflyPlateReverb-vst.dll ├── DragonflyRoomReverb-vst.x86_64-linux.so ├── OB-Xd.dll ├── Synth1_vst.x86_64-windows.dll ├── TAL-Elek7ro-II.dll ├── TAL-NoiseMaker-64.dll ├── TAL-Reverb-2-64.dll ├── Tunefish4.dll ├── TyrellN6(x64).dll └── amsynth-vst.x86_64-linux.so ├── test_vst_plugin_linux.py └── test_vst_plugin_win.py /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build-test-release-test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | create: 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | jobs: 15 | test_and_build_on_windows: 16 | runs-on: windows-latest 17 | strategy: 18 | matrix: 19 | python-version: [3.7, 3.8, 3.9] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Display python version 28 | run: python --version 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip setuptools wheel 32 | pip install flake8 33 | pip install -r requirements.txt 34 | - name: Cache VSTSDK 35 | id: vst-sdk-cache 36 | uses: actions/cache@v2 37 | with: 38 | path: build/vstsdk 39 | key: uncompressed_vstsdk_cache 40 | restore-keys: | 41 | uncompressed_vstsdk_cache 42 | - name: Build Cython module 43 | run: | 44 | python setup.py build_ext --inplace 45 | - name: Lint with flake8 46 | run: | 47 | # stop the build if there are Python syntax errors or undefined names 48 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 49 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 50 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 51 | - name: Test with pytest 52 | run: | 53 | python -m pytest 54 | - name: build dist 55 | run: | 56 | python setup.py bdist_wheel 57 | - name: Inspecting dist contents 58 | run: | 59 | ls -la dist/ 60 | - name: upload whl as an artifact 61 | uses: actions/upload-artifact@v2 62 | with: 63 | name: whl_win_${{ matrix.python-version }} 64 | path: dist/*.whl 65 | 66 | test_on_linux: 67 | runs-on: ubuntu-18.04 68 | strategy: 69 | matrix: 70 | python-version: [ 3.7, 3.8, 3.9 ] 71 | steps: 72 | - uses: actions/checkout@v2 73 | - name: Set up 74 | uses: actions/setup-python@v2 75 | with: 76 | python-version: ${{ matrix.python-version }} 77 | - name: Cache VSTSDK 78 | id: vst-sdk-cache 79 | uses: actions/cache@v2 80 | with: 81 | path: build/vstsdk 82 | key: uncompressed_vstsdk_cache 83 | - name: Install dependencies 84 | run: | 85 | python -m pip install --upgrade pip setuptools wheel 86 | pip install flake8 87 | pip install -r requirements.txt 88 | - name: Build Cython module 89 | run: | 90 | python setup.py build_ext --inplace 91 | - name: Inspecting python platform 92 | run: python inspect_platform.py 93 | - name: Lint with flake8 94 | run: | 95 | # stop the build if there are Python syntax errors or undefined names 96 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 97 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 98 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 99 | - name: Test with pytest 100 | run: | 101 | python -m pytest 102 | 103 | build_on_linux: 104 | runs-on: ubuntu-latest 105 | needs: [ test_on_linux ] 106 | steps: 107 | - uses: actions/checkout@v2 108 | - name: pwd 109 | run: pwd 110 | - name: Cache VSTSDK archive 111 | id: vst-sdk-archive-cache 112 | uses: actions/cache@v2 113 | with: 114 | path: build/vstsdk.zip 115 | key: vstsdk_cache 116 | - name: Cache VSTSDK 117 | id: vst-sdk-cache 118 | uses: actions/cache@v2 119 | with: 120 | path: build/vstsdk 121 | key: uncompressed_vstsdk_cache 122 | - name: Cache minylinux image 123 | id: manylinux-image-cache 124 | uses: actions/cache@v2 125 | with: 126 | path: build/manylinux_image 127 | key: manylinux-image-cache 128 | - name: conditionally pull and save the image 129 | run: | 130 | if [ ! -f build/manylinux_image ]; then 131 | docker pull quay.io/pypa/manylinux2010_x86_64:latest && 132 | docker save quay.io/pypa/manylinux2010_x86_64:latest > build/manylinux_image 133 | else 134 | echo "cached image is already here" 135 | fi 136 | - name: load image from saved file 137 | run: docker load -i build/manylinux_image 138 | - name: build itself 139 | run: bash build_scripts/build_on_host.sh 140 | - name: inspect dist 141 | run: tree --du dist/ 142 | - name: upload whls as an artifact 143 | uses: actions/upload-artifact@v2 144 | with: 145 | name: whl_manylinux 146 | path: dist/manylinux 147 | 148 | release: 149 | runs-on: ubuntu-latest 150 | needs: [ test_and_build_on_windows, build_on_linux ] 151 | 152 | # making a release only if this version is tagged 153 | # https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions 154 | if: ${{ contains(github.ref, 'refs/tags/') }} 155 | steps: 156 | - uses: actions/checkout@v2 157 | - name: Set up python 3.7 158 | uses: actions/setup-python@v2 159 | with: 160 | python-version: 3.7 161 | - uses: actions/download-artifact@v2 162 | with: 163 | name: whl_win_3.7 164 | path: dist 165 | - uses: actions/download-artifact@v2 166 | with: 167 | name: whl_win_3.8 168 | path: dist 169 | - uses: actions/download-artifact@v2 170 | with: 171 | name: whl_win_3.9 172 | path: dist 173 | - uses: actions/download-artifact@v2 174 | with: 175 | name: whl_manylinux 176 | path: dist 177 | - name: Inspecting dist after downloading artifacts 178 | run: | 179 | tree --du dist/ 180 | 181 | - name: Getting publishing target from 'publishing_target.txt', should be [none|test|main] 182 | id: get_publishing_target 183 | run: echo "::set-output name=publishing_target::$(cat publishing_target.txt)" 184 | 185 | - name: Publish distribution to Test PyPI if tag contains "dev" 186 | if: ${{ contains(github.ref, 'dev') }} 187 | uses: pypa/gh-action-pypi-publish@master 188 | with: 189 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 190 | repository_url: https://test.pypi.org/legacy/ 191 | 192 | - name: Publish distribution to real PyPI if tag does not contain "dev" 193 | if: ${{ ! contains(github.ref, 'dev') }} 194 | uses: pypa/gh-action-pypi-publish@master 195 | with: 196 | password: ${{ secrets.PYPI_API_TOKEN }} 197 | 198 | - name: sleep for 130s to allow subsequent jobs to fetch packages from pypi 199 | run: sleep 130 200 | 201 | test_installing_from_pypi: 202 | needs: [ release ] 203 | runs-on: ${{ matrix.os }} 204 | strategy: 205 | matrix: 206 | python-version: [3.7, 3.8, 3.9] 207 | os: [ ubuntu-latest, windows-latest] 208 | steps: 209 | - name: Set up python 210 | uses: actions/setup-python@v2 211 | with: 212 | python-version: ${{ matrix.python-version }} 213 | - name: Install pip 214 | run: python -m pip install --upgrade pip 215 | - name: inspect github.ref 216 | run: echo ${{ github.ref }} 217 | - name: extracting tag 218 | run: echo "::set-output name=version::$(echo ${{ github.ref }} | awk -F "/" '{print $3}')" 219 | id: extract_version 220 | - name: inspecting output 221 | run: echo ${{ steps.extract_version.outputs.version }} 222 | - name: installing package from test pypi (if tag is "dev") 223 | if: ${{ contains(github.ref, 'dev') }} 224 | run: pip install --index-url https://test.pypi.org/simple/ 'cython-vst-loader==${{ steps.extract_version.outputs.version }}' 225 | - name: installing package from production pypi (if tag is not "dev") 226 | if: ${{ ! contains(github.ref, 'dev') }} 227 | run: pip install 'cython-vst-loader==${{ steps.extract_version.outputs.version }}' 228 | - name: preparing test script 229 | run: | 230 | echo "from cython_vst_loader.vst_loader_wrapper import allocate_float_buffer, get_float_buffer_as_list, free_buffer, allocate_double_buffer, get_double_buffer_as_list" >> test.py 231 | echo "from cython_vst_loader.exceptions import CythonVstLoaderException" >> test.py 232 | echo "from cython_vst_loader.vst_constants import VstAEffectFlags" >> test.py 233 | echo "from cython_vst_loader.vst_event import VstEvent" >> test.py 234 | echo "from cython_vst_loader.vst_host import VstHost" >> test.py 235 | echo "pointer = allocate_float_buffer(10, 12.345)" >> test.py 236 | echo "print(str(pointer))" >> test.py 237 | echo "free_buffer(pointer)" >> test.py 238 | echo "host=VstHost(44100,512)" >> test.py 239 | echo "print('all good')" >> test.py 240 | 241 | - name: inspecting test script 242 | run: cat test.py 243 | 244 | - name: running test script 245 | run: python test.py 246 | -------------------------------------------------------------------------------- /.github/workflows/test_published_packages.yaml: -------------------------------------------------------------------------------- 1 | name: test published packages 2 | 3 | on: 4 | push: 5 | 6 | defaults: 7 | run: 8 | shell: bash 9 | 10 | jobs: 11 | test_installing_from_pypi: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | python_version: [3.7, 3.8, 3.9] 16 | os: [ ubuntu-latest, windows-latest ] 17 | steps: 18 | - name: hello world 19 | run: echo "hello-world ${{ matrix.python-version }}" 20 | - name: Set up 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install pip 25 | run: python -m pip install --upgrade pip 26 | - name: inspect github.ref 27 | run: echo ${{ github.ref }} 28 | 29 | - name: extracting tag 30 | run: echo "::set-output name=version::$(echo ${{ github.ref }} | awk -F "/" '{print $3}')" 31 | id: extract_version 32 | 33 | - name: inspecting output 34 | run: echo ${{ steps.extract_version.outputs.version }} 35 | 36 | - name: installing package from test pypi 37 | 38 | # change the version here!!!! 39 | run: pip install --index-url https://test.pypi.org/simple/ 'cython-vst-loader==0.3.dev7' 40 | 41 | - name: preparing test script 42 | run: | 43 | echo "from cython_vst_loader.vst_loader_wrapper import allocate_float_buffer, get_float_buffer_as_list, free_buffer, allocate_double_buffer, get_double_buffer_as_list" >> test.py 44 | echo "from cython_vst_loader.exceptions import CythonVstLoaderException" >> test.py 45 | echo "from cython_vst_loader.vst_constants import VstAEffectFlags" >> test.py 46 | echo "from cython_vst_loader.vst_event import VstEvent" >> test.py 47 | echo "from cython_vst_loader.vst_host import VstHost" >> test.py 48 | 49 | echo "pointer = allocate_float_buffer(10, 12.345)" >> test.py 50 | echo "print(str(pointer))" >> test.py 51 | echo "free_buffer(pointer)" >> test.py 52 | echo "host=VstHost(44100,512)" >> test.py 53 | echo "print('all good')" >> test.py 54 | 55 | - name: inspecting test script 56 | run: cat test.py 57 | 58 | - name: running test script 59 | run: python test.py 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | venv 3 | **/*.pyc 4 | 5 | build/ 6 | *.c 7 | *.so 8 | __pycache__ 9 | dist/ 10 | *.egg-info -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sergey Grechin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | recursive-include cython_vst_loader *.pyx 3 | recursive-include tests *.py 4 | recursive-include tests *.so 5 | include Makefile -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build/vstsdk 2 | 3 | build/vstsdk: build/vstsdk.zip 4 | unzip build/vstsdk.zip -d build 5 | mv "build/VST3 SDK" build/vstsdk 6 | 7 | build/vstsdk.zip: 8 | # "inspired" by https://github.com/teragonaudio/MrsWatson/blob/master/vendor/CMakeLists.txt#L379 9 | mkdir -p build 10 | curl https://www.steinberg.net/sdk_downloads/vstsdk366_27_06_2016_build_61.zip -o build/vstsdk.zip -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cython-vst-loader 2 | A loader for VST2 audio plugins providing a clean python object-oriented interface 3 | 4 | - Supported platforms: **Linux 64bit**, **Windows 64bit** 5 | - Supported python versions: **3.7**, **3.8**, **3.9** 6 | 7 | In-depth documentation: 8 | - [Usage examples](doc/usage_examples.md) 9 | - [Setting up development environment](doc/development.md) 10 | - [Building and releasing](doc/build_and_release.md) 11 | 12 | home page: https://github.com/hq9000/cython-vst-loader 13 | 14 | ## Project goals 15 | The purpose is to have a simple wrapper for VST plugins to be used in higher-level projects, such as https://github.com/hq9000/py_headless_daw 16 | 17 | ## Supported plugins 18 | 19 | Recreating a complete VST host environment in Python is a challenging task. 20 | Because of that, not every plugin will work with this wrapper, many are known not to work. 21 | Also, in case of a closed-source plugins, troubleshooting issues is almost impossible. 22 | 23 | Because of that, the loader "officially" supports (by testing) a limited number of free and (mostly) open source plugins. 24 | Other plugins may or may not work, if you discover an open source plugin that is causing issues, feel free to write a bug. 25 | 26 | The list of (tested/known-not-to-work/reportedly-working) plugins will be refreshed as new information arrives. 27 | 28 | Note: only 64-bit/VST2 plugins are currently supported. 29 | 30 | ### Plugins tested on Windows 31 | 32 | ### Synths 33 | - TAL NoiseMaker (https://tal-software.com/products/tal-noisemaker) 34 | - TAL Elec7ro (https://tal-software.com/products/tal-elek7ro) 35 | - DiscoDSP OB-Xd (https://www.discodsp.com/obxd/) 36 | - Tunefish4 (https://www.tunefish-synth.com/download) 37 | 38 | ### Effects 39 | - Dragonfly Reverb (https://michaelwillis.github.io/dragonfly-reverb/) 40 | - TAL Reverb 2 (https://tal-software.com/products/tal-reverb) 41 | 42 | ### Plugins tested on Linux 43 | 44 | ### Synths 45 | - amsynth (http://amsynth.github.io/) 46 | 47 | ### Effects 48 | - Dragonfly Reverb (https://michaelwillis.github.io/dragonfly-reverb/) 49 | 50 | ## Plugins known not to work with the loader 51 | - Synth1 52 | - TyrellN6 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ## Installation 62 | 63 | `pip install cython_vst_loader` 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /build_scripts/build_in_manylinux_container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #================================== 4 | # This script is supposed to be 5 | # run in a manylinux docker container 6 | # in creates a venv for a required python 7 | # version, install dependencies, builds extension, wheel, and, finally, 8 | # a manylinux wheel. 9 | #================================== 10 | 11 | set -e 12 | 13 | PYTHON_KEY=$1 14 | PYTHON_EXECUTABLE=/opt/python/${PYTHON_KEY}/bin/python 15 | VENV_PATH="/root/tmp_venv_${PYTHON_KEY}" 16 | SCRIPT_DIR=$(dirname "$0") 17 | PROJECT_DIR="${SCRIPT_DIR}/../" 18 | 19 | $PYTHON_EXECUTABLE -m venv ${VENV_PATH} 20 | source ${VENV_PATH}/bin/activate 21 | 22 | cd /cython-vst-loader || exit 23 | pip install -r requirements.txt 24 | pip install wheel 25 | python setup.py build_ext --inplace 26 | python setup.py bdist_wheel 27 | 28 | echo "=============== inspecting platform value ======================" 29 | python inspect_platform.py 30 | 31 | echo "=============== running a subset of unit tests ======================" 32 | # running a subset of tests which is expected to pass in manylinux container 33 | python -m pytest tests/test_buffers.py 34 | 35 | mkdir -p /cython-vst-loader/dist/manylinux 36 | ls -la /cython-vst-loader/dist 37 | auditwheel repair --plat manylinux1_x86_64 --wheel-dir /cython-vst-loader/dist/manylinux /cython-vst-loader/dist/*.whl 38 | rm -f /cython-vst-loader/dist/*.whl 39 | deactivate 40 | rm -rf ${VENV_PATH} 41 | 42 | chmod -R 777 /cython-vst-loader/dist/* -------------------------------------------------------------------------------- /build_scripts/build_in_windows.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/build_scripts/build_in_windows.sh -------------------------------------------------------------------------------- /build_scripts/build_on_host.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #================================== 4 | # This is a driver script to be 5 | # executed on a development host. 6 | # Internally, it starts manylinux docker 7 | # containers and runs build_in_manilinux_container.sh 8 | # inside the container for different 9 | # python versions. 10 | #================================== 11 | 12 | set -e 13 | 14 | SCRIPT_DIR=$(dirname "$0") 15 | PROJECT_DIR="${SCRIPT_DIR}/../" 16 | 17 | IMAGE=quay.io/pypa/manylinux2010_x86_64:latest 18 | 19 | echo "about to build for 3.9 manylinux" 20 | docker run --rm -t -v `pwd`:/cython-vst-loader $IMAGE bash /cython-vst-loader/build_scripts/build_in_manylinux_container.sh cp39-cp39 21 | echo "done building for 3.9 manylinux" 22 | 23 | echo "about to build for 3.7 manylinux" 24 | docker run --rm -t -v `pwd`:/cython-vst-loader $IMAGE bash /cython-vst-loader/build_scripts/build_in_manylinux_container.sh cp37-cp37m 25 | echo "done building for 3.7 manylinux" 26 | 27 | 28 | echo "about to build for 3.8 manylinux" 29 | docker run --rm -t -v `pwd`:/cython-vst-loader $IMAGE bash /cython-vst-loader/build_scripts/build_in_manylinux_container.sh cp38-cp38 30 | echo "done building for 3.8 manylinux" 31 | 32 | -------------------------------------------------------------------------------- /cython_vst_loader/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.c 3 | *.so 4 | __pycache__ -------------------------------------------------------------------------------- /cython_vst_loader/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/cython_vst_loader/__init__.py -------------------------------------------------------------------------------- /cython_vst_loader/dto/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/cython_vst_loader/dto/__init__.py -------------------------------------------------------------------------------- /cython_vst_loader/dto/vst_time_info.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | 5 | @dataclass 6 | class VstTimeInfo: 7 | """ 8 | This class is a python representation of the following structs defined in aeffectx.h 9 | 10 | //------------------------------------------------------------------------------------------------------- 11 | // VstTimeInfo 12 | //------------------------------------------------------------------------------------------------------- 13 | //------------------------------------------------------------------------------------------------------- 14 | /** VstTimeInfo requested via #audioMasterGetTime. @see AudioEffectX::getTimeInfo 15 | 16 | \note VstTimeInfo::samplePos :Current Position. It must always be valid, and should not cost a lot to ask for. The sample position is ahead of the time displayed to the user. In sequencer stop mode, its value does not change. A 32 bit integer is too small for sample positions, and it's a double to make it easier to convert between ppq and samples. 17 | \note VstTimeInfo::ppqPos : At tempo 120, 1 quarter makes 1/2 second, so 2.0 ppq translates to 48000 samples at 48kHz sample rate. 18 | 25 ppq is one sixteenth note then. if you need something like 480ppq, you simply multiply ppq by that scaler. 19 | \note VstTimeInfo::barStartPos : Say we're at bars/beats readout 3.3.3. That's 2 bars + 2 q + 2 sixteenth, makes 2 * 4 + 2 + .25 = 10.25 ppq. at tempo 120, that's 10.25 * .5 = 5.125 seconds, times 48000 = 246000 samples (if my calculator servers me well :-). 20 | \note VstTimeInfo::samplesToNextClock : MIDI Clock Resolution (24 per Quarter Note), can be negative the distance to the next midi clock (24 ppq, pulses per quarter) in samples. unless samplePos falls precicely on a midi clock, this will either be negative such that the previous MIDI clock is addressed, or positive when referencing the following (future) MIDI clock. 21 | */ 22 | //------------------------------------------------------------------------------------------------------- 23 | struct VstTimeInfo 24 | { 25 | //------------------------------------------------------------------------------------------------------- 26 | double samplePos; ///< current Position in audio samples (always valid) 27 | double sampleRate; ///< current Sample Rate in Herz (always valid) 28 | double nanoSeconds; ///< System Time in nanoseconds (10^-9 second) 29 | double ppqPos; ///< Musical Position, in Quarter Note (1.0 equals 1 Quarter Note) 30 | double tempo; ///< current Tempo in BPM (Beats Per Minute) 31 | double barStartPos; ///< last Bar Start Position, in Quarter Note 32 | double cycleStartPos; ///< Cycle Start (left locator), in Quarter Note 33 | double cycleEndPos; ///< Cycle End (right locator), in Quarter Note 34 | VstInt32 timeSigNumerator; ///< Time Signature Numerator (e.g. 3 for 3/4) 35 | VstInt32 timeSigDenominator; ///< Time Signature Denominator (e.g. 4 for 3/4) 36 | VstInt32 smpteOffset; ///< SMPTE offset (in SMPTE subframes (bits; 1/80 of a frame)). The current SMPTE position can be calculated using #samplePos, #sampleRate, and #smpteFrameRate. 37 | VstInt32 smpteFrameRate; ///< @see VstSmpteFrameRate 38 | VstInt32 samplesToNextClock; ///< MIDI Clock Resolution (24 Per Quarter Note), can be negative (nearest clock) 39 | VstInt32 flags; ///< @see VstTimeInfoFlags 40 | //------------------------------------------------------------------------------------------------------- 41 | }; 42 | 43 | and also some data passed in flags 44 | 45 | enum VstTimeInfoFlags 46 | { 47 | //------------------------------------------------------------------------------------------------------- 48 | kVstTransportChanged = 1, ///< indicates that play, cycle or record state has changed 1 49 | kVstTransportPlaying = 1 << 1, ///< set if Host sequencer is currently playing 2 50 | kVstTransportCycleActive = 1 << 2, ///< set if Host sequencer is in cycle mode 4 51 | kVstTransportRecording = 1 << 3, ///< set if Host sequencer is in record mode 8 52 | kVstAutomationWriting = 1 << 6, ///< set if automation write mode active (record parameter changes) 16 53 | kVstAutomationReading = 1 << 7, ///< set if automation read mode active (play parameter changes) 32 54 | 55 | kVstNanosValid = 1 << 8, ///< VstTimeInfo::nanoSeconds valid 64 56 | kVstPpqPosValid = 1 << 9, ///< VstTimeInfo::ppqPos valid 128 57 | kVstTempoValid = 1 << 10, ///< VstTimeInfo::tempo valid 256 58 | kVstBarsValid = 1 << 11, ///< VstTimeInfo::barStartPos valid 512 59 | kVstCyclePosValid = 1 << 12, ///< VstTimeInfo::cycleStartPos and VstTimeInfo::cycleEndPos valid 1024 60 | kVstTimeSigValid = 1 << 13, ///< VstTimeInfo::timeSigNumerator and VstTimeInfo::timeSigDenominator valid 61 | kVstSmpteValid = 1 << 14, ///< VstTimeInfo::smpteOffset and VstTimeInfo::smpteFrameRate valid 62 | kVstClockValid = 1 << 15 ///< VstTimeInfo::samplesToNextClock valid 63 | //------------------------------------------------------------------------------------------------------- 64 | }; 65 | 66 | """ 67 | sample_pos: float # always valid 68 | sample_rate: float # always valid 69 | nano_seconds: Optional[float] = None 70 | ppq_pos: Optional[float] = None 71 | tempo: Optional[float] = None 72 | bar_start_pos: Optional[float] = None 73 | cycle_start_pos: Optional[float] = None 74 | cycle_end_pos: Optional[float] = None 75 | time_sig_numerator: Optional[int] = None 76 | time_sig_denominator: Optional[int] = None 77 | smpte_offset: Optional[int] = None 78 | smpte_frame_rate: Optional[int] = None 79 | samples_to_next_clock: Optional[int] = None 80 | 81 | transport_changed_flag: bool = False 82 | transport_playing_flag: bool = True 83 | transport_cycle_active_flag: bool = False 84 | transport_recording_flag: bool = False 85 | automation_writing_flag: bool = False 86 | automation_reading_flag: bool = False 87 | -------------------------------------------------------------------------------- /cython_vst_loader/exceptions.py: -------------------------------------------------------------------------------- 1 | class CythonVstLoaderException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /cython_vst_loader/include/aeffectx_with_additional_structures.h: -------------------------------------------------------------------------------- 1 | #include "aeffectx.h" 2 | 3 | struct VstEvents16 4 | { 5 | //------------------------------------------------------------------------------------------------------- 6 | VstInt32 numEvents; ///< number of Events in array 7 | VstIntPtr reserved; ///< zero (Reserved for future use) 8 | VstEvent* events[16]; ///< event pointer array, variable size 9 | //------------------------------------------------------------------------------------------------------- 10 | }; 11 | 12 | 13 | struct VstEvents1024 14 | { 15 | //------------------------------------------------------------------------------------------------------- 16 | VstInt32 numEvents; ///< number of Events in array 17 | VstIntPtr reserved; ///< zero (Reserved for future use) 18 | VstEvent* events[1024]; ///< event pointer array, variable size 19 | //------------------------------------------------------------------------------------------------------- 20 | }; -------------------------------------------------------------------------------- /cython_vst_loader/vst_constants.py: -------------------------------------------------------------------------------- 1 | class AudioMasterOpcodes: 2 | # [index]: parameter index [opt]: parameter value @see AudioEffect::setParameterAutomated 3 | audioMasterAutomate = 0 4 | # [return value]: Host VST version (for example 2400 for VST 2.4) @see AudioEffect::getMasterVersion 5 | audioMasterVersion = 1 6 | # [return value]: current unique identifier on shell plug-in @see AudioEffect::getCurrentUniqueId 7 | audioMasterCurrentId = 2 8 | # no arguments @see AudioEffect::masterIdle 9 | audioMasterIdle = 3 10 | 11 | # \deprecated deprecated in VST 2.4 r2 12 | # DECLARE_VST_DEPRECATED (audioMasterPinConnected) 13 | # \deprecated deprecated in VST 2.4 14 | audioMasterWantMidi = 6 15 | 16 | # [return value]: #VstTimeInfo* or null if not supported [value]: 17 | # request mask @see VstTimeInfoFlags @see AudioEffectX::getTimeInfo 18 | audioMasterGetTime = 7 19 | # [ptr]: pointer to #VstEvents @see VstEvents @see AudioEffectX::sendVstEventsToHost 20 | audioMasterProcessEvents = 8 21 | 22 | # \deprecated deprecated in VST 2.4 23 | # DECLARE_VST_DEPRECATED (audioMasterSetTime), 24 | # \deprecated deprecated in VST 2.4 25 | # DECLARE_VST_DEPRECATED (audioMasterTempoAt), 26 | # \deprecated deprecated in VST 2.4 27 | # DECLARE_VST_DEPRECATED (audioMasterGetNumAutomatableParameters), 28 | # \deprecated deprecated in VST 2.4 29 | # DECLARE_VST_DEPRECATED (audioMasterGetParameterQuantization), 30 | 31 | # [return value]: 1 if supported @see AudioEffectX::ioChanged 32 | audioMasterIOChanged = 13 33 | 34 | # \deprecated deprecated in VST 2.4 35 | # DECLARE_VST_DEPRECATED (audioMasterNeedIdle), 36 | 37 | # [index]: new width [value]: new height [return value]: 1 if supported @see AudioEffectX::sizeWindow 38 | audioMasterSizeWindow = 15 39 | # [return value]: current sample rate @see AudioEffectX::updateSampleRate 40 | audioMasterGetSampleRate = 16 41 | # [return value]: current block size @see AudioEffectX::updateBlockSize 42 | audioMasterGetBlockSize = 17 43 | # [return value]: input latency in audio samples @see AudioEffectX::getInputLatency 44 | audioMasterGetInputLatency = 18 45 | # [return value]: output latency in audio samples @see AudioEffectX::getOutputLatency 46 | audioMasterGetOutputLatency = 19 47 | 48 | # \deprecated deprecated in VST 2.4 49 | # DECLARE_VST_DEPRECATED (audioMasterGetPreviousPlug), 50 | # \deprecated deprecated in VST 2.4 51 | # DECLARE_VST_DEPRECATED (audioMasterGetNextPlug), 52 | # \deprecated deprecated in VST 2.4 53 | # DECLARE_VST_DEPRECATED (audioMasterWillReplaceOrAccumulate), 54 | 55 | # [return value]: current process level @see VstProcessLevels 56 | audioMasterGetCurrentProcessLevel = 23 57 | # [return value]: current automation state @see VstAutomationStates 58 | audioMasterGetAutomationState = 24 59 | 60 | # [index]: numNewAudioFiles [value]: numAudioFiles [ptr]: #VstAudioFile* @see AudioEffectX::offlineStart 61 | audioMasterOfflineStart = 25 62 | # [index]: bool readSource [value]: #VstOfflineOption* @see VstOfflineOption [ptr]: #VstOfflineTask* 63 | # @see VstOfflineTask @see AudioEffectX::offlineRead 64 | audioMasterOfflineRead = 26 65 | # @see audioMasterOfflineRead @see AudioEffectX::offlineRead 66 | audioMasterOfflineWrite = 27 67 | # @see AudioEffectX::offlineGetCurrentPass 68 | audioMasterOfflineGetCurrentPass = 28 69 | # @see AudioEffectX::offlineGetCurrentMetaPass 70 | audioMasterOfflineGetCurrentMetaPass = 29 71 | 72 | # \deprecated deprecated in VST 2.4 73 | # DECLARE_VST_DEPRECATED (audioMasterSetOutputSampleRate), 74 | # \deprecated deprecated in VST 2.4 75 | # DECLARE_VST_DEPRECATED (audioMasterGetOutputSpeakerArrangement), 76 | 77 | # [ptr]: char buffer for vendor string, limited to #kVstMaxVendorStrLen @see AudioEffectX::getHostVendorString 78 | audioMasterGetVendorString = 32 79 | # [ptr]: char buffer for vendor string, limited to #kVstMaxProductStrLen @see AudioEffectX::getHostProductString 80 | audioMasterGetProductString = 33 81 | # [return value]: vendor-specific version @see AudioEffectX::getHostVendorVersion 82 | audioMasterGetVendorVersion = 34 83 | # no definition, vendor specific handling @see AudioEffectX::hostVendorSpecific 84 | audioMasterVendorSpecific = 35 85 | 86 | # \deprecated deprecated in VST 2.4 87 | # DECLARE_VST_DEPRECATED (audioMasterSetIcon), 88 | 89 | # [ptr]: "can do" string [return value]: 1 for supported 90 | audioMasterCanDo = 37 91 | # [return value]: language code @see VstHostLanguage 92 | audioMasterGetLanguage = 38 93 | 94 | # \deprecated deprecated in VST 2.4 95 | # DECLARE_VST_DEPRECATED (audioMasterOpenWindow), 96 | # \deprecated deprecated in VST 2.4 97 | # DECLARE_VST_DEPRECATED (audioMasterCloseWindow), 98 | 99 | # [return value]: FSSpec on MAC, else char* @see AudioEffectX::getDirectory 100 | audioMasterGetDirectory = 41 101 | # no arguments 102 | audioMasterUpdateDisplay = 42 103 | # [index]: parameter index @see AudioEffectX::beginEdit 104 | audioMasterBeginEdit = 43 105 | # [index]: parameter index @see AudioEffectX::endEdit 106 | audioMasterEndEdit = 44 107 | # [ptr]: VstFileSelect* [return value]: 1 if supported @see AudioEffectX::openFileSelector 108 | audioMasterOpenFileSelector = 45 109 | # [ptr]: VstFileSelect* @see AudioEffectX::closeFileSelector 110 | audioMasterCloseFileSelector = 46 111 | 112 | # \deprecated deprecated in VST 2.4 113 | # DECLARE_VST_DEPRECATED (audioMasterEditFile), 114 | 115 | # \deprecated deprecated in VST 2.4 [ptr]: char[2048] or sizeof (FSSpec) [return value]: 116 | # 1 if supported @see AudioEffectX::getChunkFile 117 | # DECLARE_VST_DEPRECATED (audioMasterGetChunkFile), 118 | 119 | # \deprecated deprecated in VST 2.4 120 | # DECLARE_VST_DEPRECATED (audioMasterGetInputSpeakerArrangement) 121 | 122 | 123 | class AEffectOpcodes: 124 | """ 125 | https://github.com/simlmx/pyvst/blob/ded9ff373f37d1cbe8948ccb053ff4849f45f4cb/pyvst/vstwrap.py#L11 126 | in SDK it is declared as enum AEffectOpcodes 127 | with a comment "Basic dispatcher Opcodes (Host to Plug-in)" 128 | Apparently, a better name would be HostToPluginDispatcherOpcodes, but let's keep it 129 | like it is to keep it aligned with sdk 130 | """ 131 | 132 | # no arguments @see AudioEffect::open 133 | effOpen = 0 134 | # no arguments @see AudioEffect::close 135 | effClose = 1 136 | 137 | # [value]: new program number @see AudioEffect::setProgram 138 | effSetProgram = 2 139 | # [return value]: current program number @see AudioEffect::getProgram 140 | effGetProgram = 3 141 | # [ptr]: char* with new program name, limited to #kVstMaxProgNameLen @see AudioEffect::setProgramName 142 | effSetProgramName = 4 143 | # [ptr]: char buffer for current program name, limited to #kVstMaxProgNameLen @see AudioEffect::getProgramName 144 | effGetProgramName = 5 145 | 146 | # [ptr]: char buffer for parameter label, limited to #kVstMaxParamStrLen @see AudioEffect::getParameterLabel 147 | effGetParamLabel = 6 148 | # [ptr]: char buffer for parameter display, limited to #kVstMaxParamStrLen @see AudioEffect::getParameterDisplay 149 | effGetParamDisplay = 7 150 | # [ptr]: char buffer for parameter name, limited to #kVstMaxParamStrLen @see AudioEffect::getParameterName 151 | effGetParamName = 8 152 | # \deprecated deprecated in VST 2.4 153 | # DECLARE_VST_DEPRECATED (effGetVu) 154 | 155 | # [opt]: new sample rate for audio processing @see AudioEffect::setSampleRate 156 | effSetSampleRate = 10 157 | # [value]: new maximum block size for audio processing @see AudioEffect::setBlockSize 158 | effSetBlockSize = 11 159 | # [value]: 0 means "turn off", 1 means "turn on" @see AudioEffect::suspend @see AudioEffect::resume 160 | effMainsChanged = 12 161 | 162 | # [ptr]: #ERect** receiving pointer to editor size @see ERect @see AEffEditor::getRect 163 | effEditGetRect = 13 164 | # [ptr]: system dependent Window pointer, e.g. HWND on Windows @see AEffEditor::open 165 | effEditOpen = 14 166 | # no arguments @see AEffEditor::close 167 | effEditClose = 15 168 | 169 | # \deprecated deprecated in VST 2.4 170 | effEditDraw = 16 171 | # deprecated deprecated in VST 2.4 172 | effEditMouse = 17 173 | # deprecated deprecated in VST 2.4 174 | effEditKey = 18 175 | 176 | # no arguments @see AEffEditor::idle 177 | effEditIdle = 19 178 | 179 | # deprecated deprecated in VST 2.4 180 | effEditTop = 20 181 | # deprecated deprecated in VST 2.4 182 | effEditSleep = 21 183 | # deprecated deprecated in VST 2.4 184 | effIdentify = 22 185 | 186 | # [ptr]: void** for chunk data address [index]: 0 for bank, 1 for program @see AudioEffect::getChunk 187 | effGetChunk = 23 188 | # [ptr]: chunk data [value]: byte size [index]: 0 for bank, 1 for program @see AudioEffect::setChunk 189 | effSetChunk = 24 190 | 191 | # [ptr]: #VstEvents* @see AudioEffectX::processEvents 192 | effProcessEvents = 25 193 | 194 | # [index]: parameter index [return value]: 1=true, 0=false @see AudioEffectX::canParameterBeAutomated 195 | effCanBeAutomated = 26 196 | # [index]: parameter index [ptr]: parameter string 197 | # [return value]: true for success @see AudioEffectX::string2parameter 198 | effString2Parameter = 27 199 | 200 | # \deprecated deprecated in VST 2.4 201 | effGetNumProgramCategories = 28 202 | 203 | # [index]: program index [ptr]: buffer for program name, limited to #kVstMaxProgNameLen 204 | # [return value]: true for success @see AudioEffectX::getProgramNameIndexed 205 | effGetProgramNameIndexed = 29 206 | 207 | # \deprecated deprecated in VST 2.4 208 | effCopyProgram = 30 209 | # \deprecated deprecated in VST 2.4 210 | effConnectInput = 31 211 | # \deprecated deprecated in VST 2.4 212 | effConnectOutput = 32 213 | 214 | # [index]: input index [ptr]: #VstPinProperties* 215 | # [return value]: 1 if supported @see AudioEffectX::getInputProperties 216 | effGetInputProperties = 33 217 | # [index]: output index [ptr]: #VstPinProperties* 218 | # [return value]: 1 if supported @see AudioEffectX::getOutputProperties 219 | effGetOutputProperties = 34 220 | # [return value]: category @see VstPlugCategory @see AudioEffectX::getPlugCategory 221 | effGetPlugCategory = 35 222 | 223 | # \deprecated deprecated in VST 2.4 224 | effGetCurrentPosition = 36 225 | # \deprecated deprecated in VST 2.4 226 | effGetDestinationBuffer = 37 227 | 228 | # [ptr]: #VstAudioFile array [value]: count [index]: start flag @see AudioEffectX::offlineNotify 229 | effOfflineNotify = 38 230 | # [ptr]: #VstOfflineTask array [value]: count @see AudioEffectX::offlinePrepare 231 | effOfflinePrepare = 39 232 | # [ptr]: #VstOfflineTask array [value]: count @see AudioEffectX::offlineRun 233 | effOfflineRun = 40 234 | 235 | # [ptr]: #VstVariableIo* @see AudioEffectX::processVariableIo 236 | effProcessVarIo = 41 237 | # [value]: input #VstSpeakerArrangement* [ptr]: 238 | # output #VstSpeakerArrangement* @see AudioEffectX::setSpeakerArrangement 239 | effSetSpeakerArrangement = 42 240 | 241 | # \deprecated deprecated in VST 2.4 242 | effSetBlockSizeAndSampleRate = 43 243 | 244 | # [value]: 1 = bypass, 0 = no bypass @see AudioEffectX::setBypass 245 | effSetBypass = 44 246 | # [ptr]: buffer for effect name limited to #kVstMaxEffectNameLen @see AudioEffectX::getEffectName 247 | effGetEffectName = 45 248 | 249 | # \deprecated deprecated in VST 2.4 250 | effGetErrorText = 46 251 | 252 | # [ptr]: buffer for effect vendor string, limited to #kVstMaxVendorStrLen @see AudioEffectX::getVendorString 253 | effGetVendorString = 47 254 | # [ptr]: buffer for effect vendor string, limited to #kVstMaxProductStrLen @see AudioEffectX::getProductString 255 | effGetProductString = 48 256 | # [return value]: vendor-specific version @see AudioEffectX::getVendorVersion 257 | effGetVendorVersion = 49 258 | # no definition, vendor specific handling @see AudioEffectX::vendorSpecific 259 | effVendorSpecific = 50 260 | # [ptr]: "can do" string [return value]: 0: "don't know" -1: "no" 1: "yes" @see AudioEffectX::canDo 261 | effCanDo = 51 262 | # [return value]: tail size (for example the reverb time of a reverb plug-in); 0 is default (return 1 for 'no tail') 263 | effGetTailSize = 52 264 | 265 | # \deprecated deprecated in VST 2.4 266 | effIdle = 53 267 | # \deprecated deprecated in VST 2.4 268 | effGetIcon = 54 269 | # \deprecated deprecated in VST 2.4 270 | effSetViewPosition = 55 271 | 272 | # [index]: parameter index [ptr]: #VstParameterProperties* 273 | # [return value]: 1 if supported @see AudioEffectX::getParameterProperties 274 | effGetParameterProperties = 56 275 | 276 | # \deprecated deprecated in VST 2.4 277 | effKeysRequired = 57 278 | 279 | # [return value]: VST version @see AudioEffectX::getVstVersion 280 | effGetVstVersion = 58 281 | 282 | # [value]: @see VstProcessPrecision @see AudioEffectX::setProcessPrecision 283 | effSetProcessPrecision = 59 284 | # [return value]: number of used MIDI input channels (1-15) @see AudioEffectX::getNumMidiInputChannels 285 | effGetNumMidiInputChannels = 60 286 | # [return value]: number of used MIDI output channels (1-15) @see AudioEffectX::getNumMidiOutputChannels 287 | effGetNumMidiOutputChannels = 61 288 | 289 | 290 | class VstStringConstants: 291 | # used for #effGetProgramName, #effSetProgramName, #effGetProgramNameIndexed 292 | kVstMaxProgNameLen = 24 293 | # used for #effGetParamLabel, #effGetParamDisplay, #effGetParamName 294 | kVstMaxParamStrLen = 8 295 | # used for #effGetVendorString, #audioMasterGetVendorString 296 | kVstMaxVendorStrLen = 64 297 | # used for #effGetProductString, #audioMasterGetProductString 298 | kVstMaxProductStrLen = 64 299 | # used for #effGetEffectName 300 | kVstMaxEffectNameLen = 32 301 | 302 | 303 | class Vst2StringConstants: 304 | # used for #MidiProgramName, #MidiProgramCategory, #MidiKeyName, #VstSpeakerProperties, #VstPinProperties 305 | kVstMaxNameLen = 64 306 | # used for #VstParameterProperties->label, #VstPinProperties->label 307 | kVstMaxLabelLen = 64 308 | # used for #VstParameterProperties->shortLabel, #VstPinProperties->shortLabel 309 | kVstMaxShortLabelLen = 8 310 | # used for #VstParameterProperties->label 311 | kVstMaxCategLabelLen = 24 312 | # used for #VstAudioFile->name 313 | kVstMaxFileNameLen = 100 314 | 315 | 316 | class VstEventTypes: 317 | kVstMidiType = 1 # < MIDI event @see VstMidiEvent 318 | # kVstAudioType = 2, #< \deprecated unused event type 319 | # DECLARE_VST_DEPRECATED (kVstVideoType) = 3, ///< \deprecated unused event type 320 | # DECLARE_VST_DEPRECATED (kVstParameterType) = 4, ///< \deprecated unused event type 321 | # DECLARE_VST_DEPRECATED (kVstTriggerType) = 5, ///< \deprecated unused event type 322 | kVstSysExType = 6 # < MIDI system exclusive @see VstMidiSysexEvent 323 | 324 | 325 | class VstAEffectFlags: 326 | # set if the plug-in provides a custom editor 327 | effFlagsHasEditor = 1 << 0 328 | # supports replacing process mode (which should the default mode in VST 2.4) 329 | effFlagsCanReplacing = 1 << 4 330 | # program data is handled in formatless chunks 331 | effFlagsProgramChunks = 1 << 5 332 | # plug-in is a synth (VSTi), Host may assign mixer channels for its outputs 333 | effFlagsIsSynth = 1 << 8 334 | # plug-in does not produce sound when input is all silence 335 | effFlagsNoSoundInStop = 1 << 9 336 | 337 | # plug-in supports double precision processing 338 | effFlagsCanDoubleReplacing = 1 << 12 339 | 340 | # \deprecated deprecated in VST 2.4 341 | # DECLARE_VST_DEPRECATED (effFlagsHasClip) = 1 << 1, 342 | # \deprecated deprecated in VST 2.4 343 | # DECLARE_VST_DEPRECATED (effFlagsHasVu) = 1 << 2, 344 | # \deprecated deprecated in VST 2.4 345 | # DECLARE_VST_DEPRECATED (effFlagsCanMono) = 1 << 3, 346 | # \deprecated deprecated in VST 2.4 347 | # DECLARE_VST_DEPRECATED (effFlagsExtIsAsync) = 1 << 10, 348 | # \deprecated deprecated in VST 2.4 349 | # DECLARE_VST_DEPRECATED (effFlagsExtHasBuffer) = 1 << 11 350 | 351 | 352 | class VstProcessLevels: 353 | """ 354 | see enum VstProcessLevels in aeffectx.h 355 | """ 356 | kVstProcessLevelUnknown = 0 # ///< not supported by Host 357 | kVstProcessLevelUser = 1 # // 1: currently in user thread (GUI) 358 | kVstProcessLevelRealtime = 2 # ///< 2: currently in audio thread (where process is called) 359 | kVstProcessLevelPrefetch = 3 # //< 3: currently in 'sequencer' thread (MIDI, timer etc) 360 | kVstProcessLevelOffline = 4 # //< 4: currently offline processing and thus in user thread 361 | -------------------------------------------------------------------------------- /cython_vst_loader/vst_event.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from cython_vst_loader.vst_constants import VstEventTypes 4 | 5 | 6 | class VstEvent: 7 | def __init__(self): 8 | self.type: Optional[int] = None 9 | self.byte_size: Optional[int] = None 10 | self.delta_frames: Optional[int] = None 11 | self.flags: Optional[int] = None 12 | self.data: Optional[bytes] = None 13 | 14 | def is_midi(self) -> bool: 15 | return self.type == VstEventTypes.kVstMidiType 16 | 17 | 18 | class VstMidiEvent(VstEvent): 19 | _NOTE_ON: str = 'note_on' 20 | _NOTE_OFF: str = 'note_off' 21 | 22 | def __init__(self, delta_frames: int): 23 | super().__init__() 24 | self.delta_frames = delta_frames 25 | self.type: int = VstEventTypes.kVstMidiType 26 | self.flags = 0 27 | self.note_length: Optional[int] = None 28 | self.note_offset: Optional[int] = None 29 | self.midi_data: bytearray = bytearray([0, 0, 0, 0]) 30 | self.detune: int = 0 31 | self.note_off_velocity: int = 0 32 | self.reserved1: int = 0 33 | self.reserved2: int = 0 34 | 35 | @classmethod 36 | def _midi_note_as_bytes(cls, note: int, velocity: int = 100, kind: str = 'note_on', channel: int = 1) -> bytes: 37 | """ 38 | borrowed from here: 39 | https://github.com/simlmx/pyvst/blob/ded9ff373f37d1cbe8948ccb053ff4849f45f4cb/pyvst/midi.py#L11 40 | 41 | :param note: 42 | :param velocity: 43 | :param kind: 44 | :param channel: Midi channel (those are 1-indexed) 45 | """ 46 | if kind == cls._NOTE_ON: 47 | kind_byte = b'\x90'[0] 48 | elif kind == cls._NOTE_OFF: 49 | kind_byte = b'\x80'[0] 50 | else: 51 | raise NotImplementedError('MIDI type {} not supported yet'.format(kind)) 52 | 53 | def _check_channel_valid(channel_to_check): 54 | if not (1 <= channel_to_check <= 16): 55 | raise ValueError('Invalid channel "{}". Must be in the [1, 16] range.' 56 | .format(channel_to_check)) 57 | 58 | _check_channel_valid(channel) 59 | 60 | return bytes([ 61 | (channel - 1) | kind_byte, 62 | note, 63 | velocity 64 | ]) 65 | 66 | 67 | class VstNoteOnMidiEvent(VstMidiEvent): 68 | def __init__(self, delta_frames: int, note: int, velocity: int, channel: int): 69 | super().__init__(delta_frames) 70 | self.midi_data = self._midi_note_as_bytes(note, velocity, self._NOTE_ON, channel) 71 | 72 | 73 | class VstNoteOffMidiEvent(VstMidiEvent): 74 | def __init__(self, delta_frames: int, note: int, channel: int): 75 | super().__init__(delta_frames) 76 | self.midi_data = self._midi_note_as_bytes(note, 0, self._NOTE_OFF, channel) 77 | -------------------------------------------------------------------------------- /cython_vst_loader/vst_host.py: -------------------------------------------------------------------------------- 1 | from cython_vst_loader.dto.vst_time_info import VstTimeInfo 2 | from cython_vst_loader.exceptions import CythonVstLoaderException 3 | from cython_vst_loader.vst_constants import AudioMasterOpcodes, VstProcessLevels 4 | 5 | 6 | class VstHost: 7 | VST_VERSION: int = 2400 8 | 9 | def __init__(self, sample_rate: int, buffer_size: int): 10 | self._sample_rate: int = sample_rate 11 | self._block_size: int = buffer_size 12 | self._bpm: float = 120.0 13 | self._sample_position: int = 0 14 | 15 | @property 16 | def bpm(self) -> float: 17 | return self._bpm 18 | 19 | @bpm.setter 20 | def bpm(self, new_pbm: float): 21 | self._bpm = new_pbm 22 | 23 | @property 24 | def sample_position(self) -> int: 25 | return self._sample_position 26 | 27 | @sample_position.setter 28 | def sample_position(self, new_sample_position: int): 29 | self._sample_position = new_sample_position 30 | 31 | # turn into props 32 | def get_sample_rate(self) -> int: 33 | return self._sample_rate 34 | 35 | def get_block_size(self) -> int: 36 | return self._block_size 37 | 38 | # noinspection PyUnusedLocal 39 | def host_callback(self, plugin_instance_pointer: int, opcode: int, index: int, value: float, ptr: int, opt: float): 40 | 41 | # print('python called host_callback with plugin instance ' + str(plugin_instance_pointer) + ' opcode: ' + str( 42 | # opcode) + " value: " + str(value)) 43 | 44 | res = None 45 | if opcode == AudioMasterOpcodes.audioMasterVersion: 46 | res = (self.VST_VERSION, None) 47 | elif opcode == AudioMasterOpcodes.audioMasterGetBlockSize: 48 | res = (self._block_size, None) 49 | elif opcode == AudioMasterOpcodes.audioMasterGetSampleRate: 50 | res = (self._sample_rate, None) 51 | elif opcode == AudioMasterOpcodes.audioMasterGetProductString: 52 | res = (0, b"CythonVstLoader") 53 | elif opcode == AudioMasterOpcodes.audioMasterWantMidi: 54 | res = (False, None) 55 | elif opcode == AudioMasterOpcodes.audioMasterGetTime: 56 | res = (0, self.generate_time_info()) 57 | elif opcode == AudioMasterOpcodes.audioMasterGetCurrentProcessLevel: 58 | res = (VstProcessLevels.kVstProcessLevelUnknown, None) 59 | elif opcode == AudioMasterOpcodes.audioMasterIOChanged: 60 | res = (0, None) 61 | elif opcode == AudioMasterOpcodes.audioMasterGetVendorString: 62 | res = (0, b"cython vst loader") 63 | elif opcode == AudioMasterOpcodes.audioMasterGetVendorVersion: 64 | res = (1, None) 65 | elif opcode == AudioMasterOpcodes.audioMasterSizeWindow: 66 | res = (0, None) 67 | else: 68 | raise CythonVstLoaderException(f"plugin-to-host opcode {str(opcode)} is not supported") 69 | 70 | return res 71 | 72 | def generate_time_info(self) -> VstTimeInfo: 73 | res = VstTimeInfo( 74 | sample_pos=self.sample_position, 75 | sample_rate=self._sample_rate, 76 | cycle_start_pos=0, 77 | cycle_end_pos=10 78 | ) 79 | 80 | return res 81 | -------------------------------------------------------------------------------- /cython_vst_loader/vst_loader_wrapper.pyx: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | from typing import Callable, List 3 | from libc.stdlib cimport malloc, free 4 | 5 | # https://cython.readthedocs.io/en/latest/src/userguide/language_basics.html#conditional-statements 6 | from cython_vst_loader.dto.vst_time_info import VstTimeInfo as PythonVstTimeInfo 7 | 8 | IF UNAME_SYSNAME != "Windows": 9 | from posix.dlfcn cimport dlopen, dlsym, RTLD_LAZY, dlerror 10 | 11 | from libc.stdint cimport int64_t, int32_t 12 | from cython_vst_loader.vst_constants import AEffectOpcodes, AudioMasterOpcodes 13 | from cython_vst_loader.vst_event import VstEvent as PythonVstEvent, VstMidiEvent as PythonVstMidiEvent 14 | import os.path 15 | from libc.string cimport memcpy 16 | 17 | # https://github.com/simlmx/pyvst/blob/ded9ff373f37d1cbe8948ccb053ff4849f45f4cb/pyvst/vstplugin.py#L23 18 | # define kEffectMagic CCONST ('V', 's', 't', 'P') 19 | # or: MAGIC = int.from_bytes(b'VstP', 'big') 20 | # 1450406992 21 | DEF MAGIC = int.from_bytes(b'VstP', 'big') 22 | 23 | # despite SDK stating that: 24 | # " 25 | # kVstMaxParamStrLen = 8, ///< used for #effGetParamLabel, #effGetParamDisplay, #effGetParamName 26 | # " 27 | # amsynth uses longer names, thus we will allocate a bigger buffer for those: 28 | DEF MAX_PARAMETER_NAME_LENGTH = 64 29 | 30 | IF UNAME_SYSNAME == "Windows": 31 | cdef extern from "windows.h": 32 | pass 33 | 34 | cdef extern from "libloaderapi.h": 35 | # windows types 36 | # https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types 37 | 38 | ctypedef void*PVOID 39 | ctypedef PVOID HANDLE 40 | ctypedef HANDLE HINSTANCE 41 | ctypedef HINSTANCE HMODULE 42 | 43 | ctypedef unsigned long DWORD 44 | ctypedef char CHAR 45 | ctypedef CHAR*LPCSTR 46 | 47 | HMODULE LoadLibraryExA(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags) 48 | HMODULE LoadLibraryA(LPCSTR lpLibFileName) 49 | DWORD GetLastError() 50 | 51 | ctypedef void*(*FARPROC)() 52 | FARPROC GetProcAddress(HMODULE hModule, LPCSTR lpProcName); 53 | 54 | cdef extern from "aeffectx_with_additional_structures.h": 55 | ctypedef int32_t VstInt32 56 | ctypedef int64_t VstIntPtr 57 | 58 | # ------------------------------------------------------------------------------------------------------- 59 | # VSTSDK: "A generic timestamped event." 60 | # ------------------------------------------------------------------------------------------------------- 61 | ctypedef struct VstEvent: 62 | VstInt32 type # < @see VstEventTypes 63 | VstInt32 byteSize # < size of this event, excl. type and byteSize 64 | VstInt32 deltaFrames # < sample frames related to the current block start sample position 65 | VstInt32 flags # < generic flags, none defined yet 66 | char data[16] # < data size may vary, depending on event type 67 | 68 | # ------------------------------------------------------------------------------------------------------- 69 | # VSTSDK: "A block of events for the current processed audio block." 70 | # ------------------------------------------------------------------------------------------------------- 71 | cdef struct VstEvents: 72 | VstInt32 numEvents # < number of Events in array 73 | VstIntPtr reserved # < zero (Reserved for future use) 74 | VstEvent*events[2] # < event pointer array, variable size 75 | 76 | cdef struct VstEvents1024: 77 | VstInt32 numEvents # < number of Events in array 78 | VstIntPtr reserved # < zero (Reserved for future use) 79 | VstEvent*events[1024] # < event pointer array, variable size 80 | 81 | cdef struct VstEvents16: 82 | VstInt32 numEvents # < number of Events in array 83 | VstIntPtr reserved # < zero (Reserved for future use) 84 | VstEvent*events[16] # < event pointer array, variable size 85 | 86 | # ------------------------------------------------------------------------------------------------------- 87 | # VSTSDK: "MIDI Event (to be casted from VstEvent)." 88 | # ------------------------------------------------------------------------------------------------------- 89 | cdef struct VstMidiEvent: 90 | VstInt32 type # < #kVstMidiType 91 | VstInt32 byteSize # < sizeof (VstMidiEvent) 92 | VstInt32 deltaFrames # < sample frames related to the current block start sample position 93 | VstInt32 flags # < @see VstMidiEventFlags 94 | VstInt32 noteLength # (in sample frames) of entire note, if available, else 0 95 | VstInt32 noteOffset # offset (in sample frames) into note from note start if available, else 0 96 | char midiData[4] # < 1 to 3 MIDI bytes; midiData[3] is reserved (zero) 97 | char detune # < -64 to +63 cents; for scales other than 'well-tempered' ('microtuning') 98 | char noteOffVelocity # Note Off Velocity [0, 127] 99 | char reserved1 # < zero (Reserved for future use) 100 | char reserved2 # < zero (Reserved for future use) 101 | 102 | # ------------------------------------------------------------------------------------------------------- 103 | # VSTSDK: "VstTimeInfo requested via #audioMasterGetTime. @see AudioEffectX::getTimeInfo " (@see aeffectx.h) 104 | # ------------------------------------------------------------------------------------------------------- 105 | cdef struct VstTimeInfo: 106 | double samplePos #< current Position in audio samples (always valid) 107 | double sampleRate #< current Sample Rate in Herz (always valid) 108 | double nanoSeconds #< System Time in nanoseconds (10^-9 second) 109 | double ppqPos #< Musical Position, in Quarter Note (1.0 equals 1 Quarter Note) 110 | double tempo #< current Tempo in BPM (Beats Per Minute) 111 | double barStartPos #< last Bar Start Position, in Quarter Note 112 | double cycleStartPos #< Cycle Start (left locator), in Quarter Note 113 | double cycleEndPos #< Cycle End (right locator), in Quarter Note 114 | VstInt32 timeSigNumerator #< Time Signature Numerator (e.g. 3 for 3/4) 115 | VstInt32 timeSigDenominator #< Time Signature Denominator (e.g. 4 for 3/4) 116 | VstInt32 smpteOffset #< SMPTE offset (in SMPTE subframes (bits; 1/80 of a frame)). The current SMPTE position can be calculated using #samplePos, #sampleRate, and #smpteFrameRate. 117 | VstInt32 smpteFrameRate #< @see VstSmpteFrameRate 118 | VstInt32 samplesToNextClock #< MIDI Clock Resolution (24 Per Quarter Note), can be negative (nearest clock) 119 | VstInt32 flags #< @see VstTimeInfoFlags 120 | 121 | # ------------------------------------------------------------------------------------------------------- 122 | # VSTSDK: "Flags used in #VstTimeInfo." (see aeffectx.h) 123 | # ------------------------------------------------------------------------------------------------------- 124 | cdef enum VstTimeInfoFlags: 125 | kVstTransportChanged = 1, #< indicates that play, cycle or record state has changed 1 126 | kVstTransportPlaying = 1 << 1, #< set if Host sequencer is currently playing 2 127 | kVstTransportCycleActive = 1 << 2, #< set if Host sequencer is in cycle mode 4 128 | kVstTransportRecording = 1 << 3, #< set if Host sequencer is in record mode 8 129 | kVstAutomationWriting = 1 << 6, #< set if automation write mode active (record parameter changes) 16 130 | kVstAutomationReading = 1 << 7, #< set if automation read mode active (play parameter changes) 32 131 | kVstNanosValid = 1 << 8, #< VstTimeInfo::nanoSeconds valid 64 132 | kVstPpqPosValid = 1 << 9, #< VstTimeInfo::ppqPos valid 128 133 | kVstTempoValid = 1 << 10, #< VstTimeInfo::tempo valid 256 134 | kVstBarsValid = 1 << 11, #< VstTimeInfo::barStartPos valid 512 135 | kVstCyclePosValid = 1 << 12, #< VstTimeInfo::cycleStartPos and VstTimeInfo::cycleEndPos valid 1024 136 | kVstTimeSigValid = 1 << 13, #< VstTimeInfo::timeSigNumerator and VstTimeInfo::timeSigDenominator valid 137 | kVstSmpteValid = 1 << 14, #< VstTimeInfo::smpteOffset and VstTimeInfo::smpteFrameRate valid 138 | kVstClockValid = 1 << 15 #< VstTimeInfo::samplesToNextClock valid 139 | 140 | # ------------------------------------------------------------------------------------------------------- 141 | # Process Levels returned by #audioMasterGetCurrentProcessLevel. */ 142 | # ------------------------------------------------------------------------------------------------------- 143 | ctypedef enum VstProcessLevels: 144 | kVstProcessLevelUnknown = 0, #///< not supported by Host 145 | kVstProcessLevelUser, #// 1: currently in user thread (GUI) 146 | kVstProcessLevelRealtime, #///< 2: currently in audio thread (where process is called) 147 | kVstProcessLevelPrefetch, #//< 3: currently in 'sequencer' thread (MIDI, timer etc) 148 | kVstProcessLevelOffline #//< 4: currently offline processing and thus in user thread 149 | 150 | # ------------------------------------------------------------------------------------------------------- 151 | # typedef VstIntPtr (VSTCALLBACK *audioMasterCallback) (AEffect* effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void* ptr, float opt); 152 | # typedef VstIntPtr (VSTCALLBACK *AEffectDispatcherProc) (AEffect* effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void* ptr, float opt); 153 | # typedef void (VSTCALLBACK *AEffectProcessProc) (AEffect* effect, float** inputs, float** outputs, VstInt32 sampleFrames); 154 | # typedef void (VSTCALLBACK *AEffectProcessDoubleProc) (AEffect* effect, double** inputs, double** outputs, VstInt32 sampleFrames); 155 | # typedef void (VSTCALLBACK *AEffectSetParameterProc) (AEffect* effect, VstInt32 index, float parameter); 156 | # typedef float (VSTCALLBACK *AEffectGetParameterProc) (AEffect* effect, VstInt32 index); 157 | # ------------------------------------------------------------------------------------------------------- 158 | ctypedef VstIntPtr (*audioMasterCallback)(AEffect*effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, 159 | void*ptr, float opt); 160 | ctypedef VstIntPtr (*AEffectDispatcherProc)(AEffect*effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, 161 | void*ptr, float opt); 162 | ctypedef void (*AEffectProcessProc)(AEffect*effect, float** inputs, float** outputs, VstInt32 sample_frames); 163 | ctypedef void (*AEffectProcessDoubleProc)(AEffect*effect, double** inputs, double** outputs, 164 | VstInt32 sample_frames); 165 | ctypedef void (*AEffectSetParameterProc)(AEffect*effect, VstInt32 index, float parameter); 166 | ctypedef float (*AEffectGetParameterProc)(AEffect*effect, VstInt32 index); 167 | ctypedef struct AEffect: 168 | VstInt32 magic 169 | 170 | AEffectDispatcherProc dispatcher 171 | AEffectSetParameterProc setParameter 172 | AEffectGetParameterProc getParameter 173 | 174 | VstInt32 numPrograms 175 | VstInt32 numParams 176 | VstInt32 numInputs 177 | VstInt32 numOutputs 178 | 179 | VstInt32 flags 180 | 181 | VstInt32 uniqueID 182 | VstInt32 version 183 | 184 | AEffectProcessProc processReplacing 185 | AEffectProcessDoubleProc processDoubleReplacing 186 | 187 | _python_host_callback = None 188 | 189 | #================================================================================= 190 | # Public 191 | #================================================================================= 192 | def host_callback_is_registered() -> bool: 193 | return _python_host_callback is not None 194 | 195 | def register_host_callback(python_host_callback: Callable)-> void: 196 | """ 197 | registers a python function to serve requests from plugins 198 | 199 | expected signature: 200 | def host_callback(plugin_instance_pointer: int, opcode: int, index: int, value: float): 201 | 202 | :param python_host_callback: 203 | :return: 204 | """ 205 | global _python_host_callback 206 | _python_host_callback = python_host_callback 207 | 208 | def get_flags(long long instance_pointer)-> int: 209 | cdef AEffect*cast_plugin_pointer = instance_pointer 210 | return cast_plugin_pointer.flags 211 | 212 | def create_plugin(path_to_so: bytes)-> int: 213 | if not os.path.exists(path_to_so): 214 | raise Exception('plugin file does not exist: ' + str(path_to_so)) 215 | 216 | global _python_host_callback 217 | if _python_host_callback is None: 218 | raise Exception('python host callback has not been registered') 219 | 220 | c_plugin_pointer = _load_vst(path_to_so) 221 | 222 | if MAGIC != c_plugin_pointer.magic: 223 | raise Exception('MAGIC is wrong') 224 | 225 | return c_plugin_pointer 226 | 227 | def allocate_float_buffer(int size, float fill_with) -> int: 228 | cdef float *ptr = malloc(size * sizeof(float)) 229 | for i in range(0, size): 230 | ptr[i] = fill_with 231 | return ptr 232 | 233 | def allocate_double_buffer(int size, double fill_with) -> int: 234 | cdef double *ptr = malloc(size * sizeof(double)) 235 | for i in range(0, size): 236 | ptr[i] = fill_with 237 | return ptr 238 | 239 | def get_float_buffer_as_list(long long buffer_pointer, int size) -> List[float]: 240 | cdef float *ptr = buffer_pointer 241 | res = [] 242 | for i in range(0, size): 243 | res.append(float(ptr[i])) 244 | 245 | return res 246 | 247 | def get_double_buffer_as_list(long long buffer_pointer, int size) -> List[float]: 248 | cdef double *ptr = buffer_pointer 249 | res = [] 250 | for i in range(0, size): 251 | res.append(float(ptr[i])) 252 | 253 | return res 254 | 255 | def free_buffer(long long pointer): 256 | free( pointer) 257 | 258 | # maximum number of channels a plugin can support 259 | DEF MAX_CHANNELS=10 260 | 261 | # noinspection DuplicatedCode 262 | def process_replacing(long long plugin_pointer, input_pointer_list: List[int], output_pointer_list: List[int], 263 | num_frames: int): 264 | cdef AEffect*cast_plugin_pointer = plugin_pointer 265 | 266 | num_input_channels = len(input_pointer_list) 267 | num_output_channels = len(output_pointer_list) 268 | 269 | cdef float *input_pointers[MAX_CHANNELS] 270 | cdef float *output_pointers[MAX_CHANNELS] 271 | 272 | cdef long long tmp 273 | 274 | for index, pointer in enumerate(input_pointer_list): 275 | tmp = pointer 276 | input_pointers[index] = tmp 277 | 278 | for index, pointer in enumerate(output_pointer_list): 279 | tmp = pointer 280 | output_pointers[index] = tmp 281 | 282 | cast_plugin_pointer.processReplacing(cast_plugin_pointer, input_pointers, output_pointers, num_frames) 283 | 284 | # noinspection DuplicatedCode 285 | def process_double_replacing(long long plugin_pointer, input_pointer_list: List[int], output_pointer_list: List[int], 286 | num_frames: int): 287 | cdef AEffect*cast_plugin_pointer = plugin_pointer 288 | 289 | num_input_channels = len(input_pointer_list) 290 | num_output_channels = len(output_pointer_list) 291 | 292 | cdef double *input_pointers[MAX_CHANNELS] 293 | cdef double *output_pointers[MAX_CHANNELS] 294 | 295 | cdef long tmp 296 | 297 | for index, pointer in enumerate(input_pointer_list): 298 | tmp = pointer 299 | input_pointers[index] = tmp 300 | 301 | for index, pointer in enumerate(output_pointer_list): 302 | tmp = pointer 303 | output_pointers[index] = tmp 304 | 305 | cast_plugin_pointer.processDoubleReplacing(cast_plugin_pointer, input_pointers, output_pointers, num_frames) 306 | 307 | def set_parameter(long long plugin_pointer, int index, float value): 308 | cdef AEffect *cast_plugin_pointer = plugin_pointer 309 | cast_plugin_pointer.setParameter(cast_plugin_pointer, index, value) 310 | 311 | def get_parameter(long long plugin_pointer, int index)-> float: 312 | cdef AEffect *cast_plugin_pointer = plugin_pointer 313 | return cast_plugin_pointer.getParameter(cast_plugin_pointer, index) 314 | 315 | def start_plugin(long long plugin_instance_pointer, int sample_rate, int block_size): 316 | cdef float sample_rate_as_float = sample_rate 317 | cdef AEffect*cast_plugin_pointer = plugin_instance_pointer 318 | 319 | cast_plugin_pointer.dispatcher(cast_plugin_pointer, AEffectOpcodes.effOpen, 0, 0, NULL, 0.0) 320 | cast_plugin_pointer.dispatcher(cast_plugin_pointer, AEffectOpcodes.effSetSampleRate, 0, 0, NULL, sample_rate) 321 | cast_plugin_pointer.dispatcher(cast_plugin_pointer, AEffectOpcodes.effSetBlockSize, 0, block_size, NULL, 0.0) 322 | #_resume_plugin(cast_plugin_pointer) 323 | 324 | def get_num_parameters(long long plugin_pointer) -> int: 325 | cdef AEffect *cast_plugin_pointer = plugin_pointer 326 | return cast_plugin_pointer.numParams 327 | 328 | def get_num_inputs(long long plugin_pointer) -> int: 329 | cdef AEffect *cast_plugin_pointer = plugin_pointer 330 | return cast_plugin_pointer.numInputs 331 | 332 | def get_num_outputs(long long plugin_pointer) -> int: 333 | cdef AEffect *cast_plugin_pointer = plugin_pointer 334 | return cast_plugin_pointer.numOutputs 335 | 336 | def get_num_programs(long long plugin_pointer) -> int: 337 | cdef AEffect *cast_plugin_pointer = plugin_pointer 338 | return cast_plugin_pointer.numPrograms 339 | 340 | def get_parameter_name(long long plugin_pointer, int param_index): 341 | cdef void *buffer = malloc(MAX_PARAMETER_NAME_LENGTH * sizeof(char)) 342 | dispatch_to_plugin(plugin_pointer, AEffectOpcodes.effGetParamName, param_index, 0, buffer, 0.0) 343 | cdef char *res = buffer 344 | return res 345 | 346 | def dispatch_to_plugin(long long plugin_pointer, VstInt32 opcode, VstInt32 index, VstInt32 value, long long ptr, 347 | float opt) -> int: 348 | cdef AEffect *cast_plugin_pointer = plugin_pointer 349 | cdef void *cast_parameter_pointer = ptr 350 | # AEffect* effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void* ptr, float opt 351 | return cast_plugin_pointer.dispatcher(cast_plugin_pointer, opcode, index, value, cast_parameter_pointer, opt) 352 | 353 | def process_events_16(long long plugin_pointer, python_events: List[PythonVstEvent]): 354 | """ 355 | processes at most 16 events 356 | 357 | Why? I couldn't find a way to pass a dynamically sized list of events, so I introduced 358 | two versions of the function the one that sends at most 16 events, and the one for the case of 1024. 359 | 360 | This two stepped approach is to avoid unnecessarily allocating too much space in stack when normally this number is 361 | well beyond 16. 362 | """ 363 | cdef VstEvents16 events 364 | _process_events_variable_length(plugin_pointer, python_events, &events) 365 | 366 | def process_events_1024(long long plugin_pointer, python_events: List[PythonVstEvent]): 367 | """ 368 | processes at most 1024 events 369 | """ 370 | cdef VstEvents1024 events 371 | _process_events_variable_length(plugin_pointer, python_events, &events) 372 | 373 | def _process_events_variable_length(long long plugin_pointer, python_events: List[PythonVstEvent], 374 | long long passed_events_pointer): 375 | python_midi_events = [python_event for python_event in python_events if python_event.is_midi()] 376 | 377 | cdef AEffect*cast_plugin_pointer = plugin_pointer 378 | cdef VstMidiEvent *c_midi_events = malloc(len(python_midi_events) * sizeof(VstMidiEvent)) 379 | cdef VstEvents1024 *events = passed_events_pointer 380 | cdef VstMidiEvent *c_event_pointer = NULL 381 | events.numEvents = len(python_midi_events) 382 | 383 | for position, python_event in enumerate(python_midi_events): 384 | _convert_python_midi_event_into_c(python_event, &c_midi_events[position]) 385 | events.events[position] = &c_midi_events[position] 386 | 387 | _process_events(cast_plugin_pointer, events) 388 | 389 | free(c_midi_events) 390 | 391 | #================================================================================= 392 | # Private 393 | #================================================================================= 394 | cdef _process_events(AEffect *plugin, VstEvents *events): 395 | plugin.dispatcher(plugin, AEffectOpcodes.effProcessEvents, 0, 0, events, 0.0) 396 | 397 | cdef _convert_python_midi_event_into_c(python_event: PythonVstMidiEvent, VstMidiEvent *c_event_pointer): 398 | c_event_pointer.type = python_event.type 399 | c_event_pointer.byteSize = sizeof(VstMidiEvent) 400 | c_event_pointer.deltaFrames = python_event.delta_frames 401 | c_event_pointer.flags = python_event.flags 402 | 403 | for n in [0, 1, 2]: 404 | c_event_pointer.midiData[n] = python_event.midi_data[n] 405 | 406 | c_event_pointer.detune = python_event.detune 407 | c_event_pointer.noteOffVelocity = python_event.note_off_velocity 408 | c_event_pointer.reserved1 = python_event.reserved1 409 | c_event_pointer.reserved2 = python_event.reserved2 410 | 411 | cdef VstIntPtr _c_host_callback(AEffect*effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void *ptr, float opt): 412 | """ 413 | A C-level entry for accessing host through sending "opcodes" 414 | 415 | :param effect: 416 | :param opcode: 417 | :param index: 418 | :param value: 419 | :param ptr: 420 | :param opt: 421 | :return: 422 | """ 423 | #print("_c_host_callback called with opcode " + str(opcode) + " index = " + str(index) + " value: ") 424 | 425 | if opcode == AudioMasterOpcodes.audioMasterGetTime: 426 | return _c_host_callback_for_gettimeinfo(effect, opcode, index, value, ptr, opt) 427 | 428 | cdef long long plugin_instance_identity = effect 429 | cdef VstIntPtr result 430 | (return_code, data_to_write) = _python_host_callback(plugin_instance_identity, opcode, index, value, 431 | ptr, opt) 432 | result = return_code 433 | if data_to_write is not None: 434 | if isinstance(data_to_write, bytes): 435 | memcpy(ptr, data_to_write, len(data_to_write)) 436 | else: 437 | raise Exception("this type of return value is not supported here (error: 93828ccb)") 438 | return result 439 | 440 | cdef VstIntPtr _c_host_callback_for_gettimeinfo(AEffect*effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, 441 | void *ptr, float opt): 442 | """ 443 | A specialized branch of _c_host_callback specifically dealing with getTimeInfo case 444 | 445 | also see implementation of VstTimeInfo* AudioEffectX::getTimeInfo (VstInt32 filter) 446 | 447 | :param effect: 448 | :param opcode: 449 | :param index: 450 | :param value: 451 | :param ptr: 452 | :param opt: 453 | :return: 454 | """ 455 | cdef long long plugin_instance_identity = effect 456 | (return_code, data_to_write) = _python_host_callback(plugin_instance_identity, opcode, index, value, 457 | ptr, opt) 458 | 459 | cdef VstTimeInfo *vst_time_info_ptr = NULL 460 | 461 | if isinstance(data_to_write, PythonVstTimeInfo): 462 | vst_time_info_ptr = malloc( 463 | sizeof(VstTimeInfo)) # this is obviously a memory leak, unless plugins free the mem themselves (I doubt), we'll have to take care of it somehow 464 | _copy_python_vst_time_info_into_c_version(data_to_write, vst_time_info_ptr) 465 | return vst_time_info_ptr 466 | else: 467 | raise Exception("instance of PythonVstTimeInfo was expected (error: 094e2dc1)") 468 | 469 | cdef void _copy_python_vst_time_info_into_c_version(python_version: PythonVstTimeInfo, VstTimeInfo *c_version): 470 | """ 471 | converts (by copying into a pre-allocated memory) a python DTO for VstTimeInfo into C struct 472 | 473 | :param python_version: 474 | :param c_version: 475 | :return: 476 | """ 477 | cdef VstInt32 flags = 0 478 | 479 | if python_version.sample_pos is not None: 480 | c_version.samplePos = python_version.sample_pos 481 | else: 482 | raise Exception('sample_pos should be always present ("always valid") (error: c550e595)') 483 | 484 | if python_version.sample_rate is not None: 485 | c_version.sampleRate = python_version.sample_rate 486 | else: 487 | raise Exception('sample_rate should be always present ("always valid") (error: a47a8e4e)') 488 | 489 | if python_version.nano_seconds is not None: 490 | c_version.nanoSeconds = python_version.nano_seconds 491 | flags |= kVstNanosValid 492 | 493 | if python_version.ppq_pos is not None: 494 | c_version.ppqPos = python_version.ppq_pos 495 | flags |= kVstPpqPosValid 496 | 497 | if python_version.tempo is not None: 498 | c_version.tempo = python_version.tempo 499 | flags |= kVstTempoValid 500 | 501 | if python_version.bar_start_pos is not None: 502 | c_version.barStartPos = python_version.bar_start_pos 503 | flags |= kVstBarsValid 504 | 505 | cycle_positions = [python_version.cycle_start_pos, python_version.cycle_end_pos] 506 | 507 | if all(x is not None for x in cycle_positions): 508 | c_version.cycleStartPos = python_version.cycle_start_pos 509 | c_version.cycleEndPos = python_version.cycle_end_pos 510 | flags |= kVstCyclePosValid 511 | elif any(x is not None for x in cycle_positions): 512 | raise Exception("either both or none of cycle start/end should be supplied (error: c4a02afb)") 513 | 514 | time_sig_values = [python_version.time_sig_numerator, python_version.time_sig_denominator] 515 | 516 | if all(x is not None for x in time_sig_values): 517 | c_version.timeSigNumerator = python_version.time_sig_numerator 518 | c_version.timeSigDenominator = python_version.time_sig_denominator 519 | flags |= kVstTimeSigValid 520 | elif any(x is not None for x in time_sig_values): 521 | raise Exception( 522 | "either both or none of time signature numerator/denominator should be supplied (error: bc0e3784)") 523 | 524 | smpte_values = [python_version.smpte_offset, python_version.smpte_frame_rate] 525 | 526 | if all(x is not None for x in smpte_values): 527 | c_version.smpteOffset = python_version.smpte_offset 528 | c_version.smpteFrameRate = python_version.smpte_frame_rate 529 | flags |= kVstSmpteValid 530 | elif any(x is not None for x in smpte_values): 531 | raise Exception("either both or none of smpte offset/framerate should be supplied (error: 48054728)") 532 | 533 | if python_version.samples_to_next_clock is not None: 534 | c_version.samplesToNextClock = python_version.samples_to_next_clock 535 | flags |= kVstClockValid 536 | 537 | # now dealing with boolean flags 538 | if python_version.transport_changed_flag: 539 | flags |= kVstTransportChanged 540 | 541 | if python_version.transport_playing_flag: 542 | flags |= kVstTransportPlaying 543 | 544 | if python_version.transport_cycle_active_flag: 545 | flags |= kVstTransportCycleActive 546 | 547 | if python_version.transport_recording_flag: 548 | flags |= kVstTransportRecording 549 | 550 | if python_version.automation_writing_flag: 551 | flags |= kVstAutomationWriting 552 | 553 | if python_version.automation_reading_flag: 554 | flags |= kVstAutomationReading 555 | 556 | c_version.flags = flags 557 | 558 | ctypedef AEffect *(*vstPluginFuncPtr)(audioMasterCallback host) 559 | 560 | cdef AEffect *_load_vst(char *path_to_so) except? 0: 561 | """ 562 | the main function implementing a cross-platform logic of loading a plugin (.so in linux and .dll in windows) 563 | 564 | :param path_to_so: 565 | :return: 566 | """ 567 | # https://cython.readthedocs.io/en/latest/src/userguide/language_basics.html#conditional-statements 568 | IF UNAME_SYSNAME != "Windows": 569 | """ 570 | main loader function for linux 571 | """ 572 | cdef char *entry_function_name = "VSTPluginMain" 573 | cdef void *handle = dlopen(path_to_so, RTLD_LAZY) 574 | cdef char*error 575 | if handle is NULL: 576 | error = dlerror() 577 | raise Exception(b"null pointer handle as a result of dlopen: " + error) 578 | 579 | # some plugins seem to use "main" instead of "VSTPluginMain" 580 | cdef vstPluginFuncPtr entry_function = dlsym(handle, b"main") 581 | 582 | if entry_function is NULL: 583 | error = dlerror() 584 | raise Exception(b"null pointer when looking up entry function: " + error) 585 | 586 | cdef AEffect *plugin_ptr = entry_function(_c_host_callback) 587 | return plugin_ptr 588 | ELSE: 589 | """ 590 | main loader function for Windows 591 | """ 592 | # https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa 593 | cdef HMODULE handle = LoadLibraryA(path_to_so) 594 | cdef DWORD error_code = GetLastError() 595 | 596 | if handle is NULL: 597 | print(b"null pointer when loading a DLL. Error code = " + str(error_code)) 598 | raise Exception(b"null pointer when loading a DLL. Error code = " + str(error_code)) 599 | 600 | cdef vstPluginFuncPtr entry_function = GetProcAddress(handle, "VSTPluginMain"); 601 | if entry_function is NULL: 602 | print(b"null pointer when obtaining an address of the entry function. Error code = " + str(error_code)) 603 | raise Exception( 604 | b"null pointer when obtaining an address of the entry function. Error code = " + str(error_code)) 605 | 606 | cdef AEffect *plugin_ptr = entry_function(_c_host_callback) 607 | plugin_ptr.dispatcher(plugin_ptr, AEffectOpcodes.effOpen, 0, 0, NULL, 0.0) 608 | return plugin_ptr 609 | 610 | cdef _suspend_plugin(AEffect *plugin): 611 | plugin.dispatcher(plugin, AEffectOpcodes.effMainsChanged, 0, 0, NULL, 0.0) 612 | pass 613 | 614 | cdef _resume_plugin(AEffect *plugin): 615 | plugin.dispatcher(plugin, AEffectOpcodes.effMainsChanged, 0, 1, NULL, 0.0) 616 | pass 617 | 618 | # on bool return: https://stackoverflow.com/questions/24659723/cython-issue-bool-is-not-a-type-identifier 619 | cdef bint _plugin_can_do(AEffect *plugin, char *can_do_string): 620 | return plugin.dispatcher(plugin, AEffectOpcodes.effCanDo, 0, 0, can_do_string, 0.0) > 0 621 | 622 | cdef void _process_midi(AEffect*plugin, VstEvents*events): 623 | plugin.dispatcher(plugin, AEffectOpcodes.effProcessEvents, 0, 0, events, 0.0) 624 | -------------------------------------------------------------------------------- /cython_vst_loader/vst_plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional, Dict, List 3 | 4 | # my Pycharm does not resolve defs from the pyx file 5 | # noinspection PyUnresolvedReferences 6 | from cython_vst_loader.vst_loader_wrapper import create_plugin, register_host_callback, host_callback_is_registered, \ 7 | get_num_parameters, get_parameter, set_parameter, get_num_inputs, get_num_outputs, get_num_programs, \ 8 | process_replacing, get_flags, process_double_replacing, get_parameter_name, \ 9 | start_plugin, process_events_16, process_events_1024 10 | 11 | from cython_vst_loader.exceptions import CythonVstLoaderException 12 | from cython_vst_loader.vst_constants import VstAEffectFlags 13 | from cython_vst_loader.vst_event import VstEvent 14 | from cython_vst_loader.vst_host import VstHost 15 | 16 | 17 | class VstPlugin: 18 | MAX_EVENTS_PER_PROCESS_EVENTS_CALL = 1024 19 | 20 | # needed for temporarily setting the host 21 | # on plugin initialization, 22 | # reason: plugins might want to do some host callbacks 23 | # right in the their entry functions 24 | _temporary_context_host: Optional[VstHost] = None 25 | 26 | # a map for maintaining info 27 | # on which plugin belongs to which host 28 | _plugin_host_map: Dict[int, VstHost] = {} 29 | 30 | def __init__(self, path_to_shared_library: bytes, host: VstHost): 31 | if not host_callback_is_registered(): 32 | register_host_callback(self._global_host_callback) 33 | 34 | if not os.path.exists(path_to_shared_library): 35 | raise FileNotFoundError('plugin file not found: ' + str(path_to_shared_library)) 36 | 37 | if not os.path.isfile(path_to_shared_library): 38 | raise FileNotFoundError('plugin path does not point to a file: ' + str(path_to_shared_library)) 39 | 40 | VstPlugin._temporary_context_host = host 41 | self._instance_pointer: int = create_plugin(path_to_shared_library) 42 | 43 | self._plugin_host_map[self._instance_pointer] = host 44 | self._allows_double_precision_cached_value: Optional[bool] = None 45 | 46 | if self._instance_pointer not in self._plugin_host_map: 47 | raise Exception("host instance not found for plugin with identity " + str(self._instance_pointer)) 48 | 49 | VstPlugin._temporary_context_host = None 50 | 51 | start_plugin(self._instance_pointer, host.get_sample_rate(), host.get_block_size()) 52 | 53 | @classmethod 54 | def _global_host_callback(cls, plugin_instance_pointer: int, opcode: int, index: int, value: float, ptr: int, 55 | opt: float): 56 | 57 | if cls._temporary_context_host is not None: 58 | host = cls._temporary_context_host 59 | else: 60 | if plugin_instance_pointer not in cls._plugin_host_map: 61 | raise CythonVstLoaderException( 62 | 'plugin identity ' + str(plugin_instance_pointer) + ' not found in host map') 63 | 64 | host = cls._plugin_host_map[plugin_instance_pointer] 65 | if host is None: 66 | raise CythonVstLoaderException('host is not registered for this plugin') 67 | 68 | res = host.host_callback(plugin_instance_pointer, opcode, index, value, ptr, opt) 69 | return res 70 | 71 | def get_num_parameters(self) -> int: 72 | return get_num_parameters(self._instance_pointer) 73 | 74 | def get_parameter_value(self, parameter_index: int) -> float: 75 | self._validate_parameter_index(parameter_index) 76 | return get_parameter(self._instance_pointer, parameter_index) 77 | 78 | def set_parameter_value(self, parameter_index: int, value: float): 79 | self._validate_parameter_index(parameter_index) 80 | set_parameter(self._instance_pointer, parameter_index, value) 81 | 82 | def get_num_input_channels(self) -> int: 83 | return get_num_inputs(self._instance_pointer) 84 | 85 | def get_num_output_channels(self) -> int: 86 | return get_num_outputs(self._instance_pointer) 87 | 88 | def get_num_programs(self) -> int: 89 | return get_num_programs(self._instance_pointer) 90 | 91 | def process_events(self, events: List[VstEvent]): 92 | if len(events) > self.MAX_EVENTS_PER_PROCESS_EVENTS_CALL: 93 | raise ValueError( 94 | f"passing more than {str(self.MAX_EVENTS_PER_PROCESS_EVENTS_CALL)} is not supported (error: edaa3dff)") 95 | 96 | if len(events) <= 16: 97 | process_events_16(self._instance_pointer, events) 98 | else: 99 | process_events_1024(self._instance_pointer, events) 100 | 101 | def process_replacing(self, input_channel_pointers: List[int], output_channel_pointers: List[int], block_size: int): 102 | 103 | process_replacing(self._instance_pointer, input_channel_pointers, output_channel_pointers, block_size) 104 | 105 | def process_double_replacing(self, input_channel_pointers: List[int], output_channel_pointers: List[int], 106 | block_size: int): 107 | if not self.allows_double_precision(): 108 | raise CythonVstLoaderException('this plugin does not support double precision') 109 | 110 | process_double_replacing(self._instance_pointer, input_channel_pointers, output_channel_pointers, block_size) 111 | 112 | def _validate_parameter_index(self, index: int): 113 | if index < 0 or index > self.get_num_parameters() - 1: 114 | raise CythonVstLoaderException('requested parameter index is out of range: ' + str(index)) 115 | 116 | def is_synth(self) -> bool: 117 | return bool(get_flags(self._instance_pointer) & VstAEffectFlags.effFlagsIsSynth) 118 | 119 | def get_parameter_name(self, param_index: int) -> bytes: 120 | return get_parameter_name(self._instance_pointer, param_index) 121 | 122 | def allows_double_precision(self) -> bool: 123 | if self._allows_double_precision_cached_value is None: 124 | self._allows_double_precision_cached_value = bool( 125 | get_flags(self._instance_pointer) & VstAEffectFlags.effFlagsCanDoubleReplacing) 126 | return self._allows_double_precision_cached_value 127 | -------------------------------------------------------------------------------- /doc/build_and_release.md: -------------------------------------------------------------------------------- 1 | # Build and Release 2 | 3 | ## Overview 4 | Binary wheels are distributed for `manylinux1_x86_64` and `win_amd64` platforms for python versions `3.7-3.9` 5 | 6 | Building and publishing in PyPI are automated through github actions. The main workflow file is `.github/worksflows/build.yaml` 7 | 8 | ## When the workflow is run: 9 | - on every push to `master` branch 10 | - on every push to a branch that has an open `master`-targeted pull request 11 | 12 | ## Publishing to PyPI 13 | Publishing only happens when a `tag` is put to a commit. Commits without tags still invoke builds, but the final publishing steps are omitted. 14 | 15 | ## Versioning 16 | Care must be taken when putting tags: 17 | 18 | - tags should use semantic versioning 19 | - tags of the form `1.2.3` are considered "production" and builds are pushed to the main PyPI 20 | - tags of the form `1.2.dev3` are considered "development" and corresponding builds are pushed to test PyPI 21 | 22 | tags of any other format are not supported and behavior is undefined. 23 | 24 | To conveniently put tags, github "releases" can be used: 25 | ![create_release_github](https://user-images.githubusercontent.com/21345604/112721460-ea6c0e00-8f14-11eb-829d-e2ee3f3906b9.gif) 26 | 27 | ## Checking uploaded releases 28 | 29 | Normally, the releases uploaded to PyPI are checked automatically in the workflow. 30 | However, you might want to check a given release separately for some reason. 31 | 32 | One situation that can lead to this is the delay of pypi that makes the last jobs unable to download the package. 33 | Although there is a delay inserted into the workflow to account for that, it can theoretically break at some point. 34 | 35 | For that, there is a dedicated workflow called `test_published_packages.yaml`. 36 | Beware that you will have to change the version to be tested in the workflow. -------------------------------------------------------------------------------- /doc/development.md: -------------------------------------------------------------------------------- 1 | # Development Environment 2 | 3 | ## Generally useful commands 4 | 5 | The following steps assume that supported `venv` is activated 6 | 7 | - Installing dependencies 8 | 9 | - `pip install -r requirements.txt` 10 | 11 | - (Re)building the extension 12 | 13 | - this command builds the extension: `python setup.py build_ext --inplace` 14 | - Running tests 15 | 16 | - `python -m pytest` in the root of the project 17 | 18 | ## Setting up development environment on Linux 19 | 20 | - (on Ubuntu) install gcc `apt-get install gcc` 21 | 22 | ## Setting up development environment in Windows 23 | 24 | ### Installing the Visual Studio compiler 25 | 1. to get a compiler, I'm now installing a "Visual Studio Community" from here: https://visualstudio.microsoft.com/vs/community/ 26 | 2. after launching the installer, from the many options it gives me, I only choose "development of classical apps", the expected installation size is terrifying 7Gbs. 27 | 28 | ### Setting up a linux-like environment using git-bash 29 | 30 | #### Installing git-bash 31 | 32 | Git bash comes with git for windows 33 | 34 | #### Installing make and wget 35 | 36 | These two are needed to download VSTSDK. To install them into the mingw supplied withing git bash, I followed this instruction [here](https://gist.github.com/evanwill/0207876c3243bbb6863e65ec5dc3f058): 37 | below is an extract from that doc: 38 | 39 | > ## Make 40 | > 41 | > Keep in mind you can easy add `make`, but it doesn't come packaged with all the standard UNIX build toolchain--so you will have to ensure those are installed *and* on your PATH, or you will encounter endless error messages. 42 | > - Go to [ezwinports](https://sourceforge.net/projects/ezwinports/files/). 43 | > - Download `make-4.1-2-without-guile-w32-bin.zip` (get the version without guile). 44 | > - Extract zip. 45 | > - Copy the contents to your `Git\mingw64\` merging the folders, but do NOT overwrite/replace any existing files. 46 | > 47 | > ## Wget 48 | > - Download the latest wget binary for windows from [eternallybored](https://eternallybored.org/misc/wget/) (they are available as a zip with documentation, or just an exe) 49 | > - If you downloaded the zip, extract all (if windows built in zip utility gives an error, use [7-zip](http://www.7-zip.org/)). 50 | > - Rename the file `wget64.exe` to `wget.exe` if necessary. 51 | > - Move `wget.exe` to your `Git\mingw64\bin\`. 52 | 53 | Essentially, it all comes down to copying a few additional files into `c:\Program Files\Git\mingw64` as shown on the screenshots below: 54 | 55 | ![image](https://user-images.githubusercontent.com/21345604/111060267-9db41c00-84ac-11eb-8d14-bc7fb1f0f484.png) 56 | ![image](https://user-images.githubusercontent.com/21345604/111060356-61cd8680-84ad-11eb-997a-0044763fd7d9.png) 57 | 58 | ### Install Pythons and create venvs 59 | 60 | Download from here `https://www.python.org/downloads/` and install python 3.7, 3.8, 3.9 61 | 62 | In my case, python binaries end up here: `C:\Users\user\AppData\Local\Programs\Python\` 63 | 64 | ![image](https://user-images.githubusercontent.com/21345604/111866019-71e8d880-897b-11eb-8870-91319cbbdaa4.png) 65 | 66 | create venvs: 67 | 68 | - `/c/Users/user/AppData/Local/Programs/Python/Python37/python.exe -m venv /c/home/em/test_python/venv37` 69 | - `/c/Users/user/AppData/Local/Programs/Python/Python38/python.exe -m venv /c/home/em/test_python/venv38` 70 | - `/c/Users/user/AppData/Local/Programs/Python/Python39/python.exe -m venv /c/home/em/test_python/venv39` 71 | 72 | 73 | #### Building the extension 74 | 75 | - launch git-bash 76 | - Activate venv: `source venv/Scripts/activate` 77 | - finally, build `python setup.py build_ext --inplace` -------------------------------------------------------------------------------- /doc/git/hooks/pre-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This way you can customize which branches should be skipped when 4 | # prepending commit message. 5 | 6 | if [ -z "$BRANCHES_TO_SKIP" ]; then 7 | BRANCHES_TO_SKIP=(master develop test) 8 | fi 9 | 10 | BRANCH_NAME=$(git symbolic-ref --short HEAD) 11 | REGEXP='([0-9]+)' 12 | ISSUE_NUMBER=9999999 13 | 14 | if [[ ${BRANCH_NAME} =~ ${REGEXP} ]]; then 15 | ISSUE_NUMBER=$BASH_REMATCH 16 | else 17 | echo "does not match" 18 | fi 19 | 20 | 21 | BRANCH_EXCLUDED=$(printf "%s\n" "${BRANCHES_TO_SKIP[@]}" | grep -c "^$BRANCH_NAME$") 22 | BRANCH_IN_COMMIT=$(grep -c "\[$BRANCH_NAME\]" $1) 23 | 24 | if [ -n "$BRANCH_NAME" ] && ! [[ $BRANCH_EXCLUDED -eq 1 ]] && ! [[ $BRANCH_IN_COMMIT -ge 2 ]]; then 25 | original_message=`cat $1` 26 | echo [${BRANCH_NAME} \#${ISSUE_NUMBER}] $original_message > $1 27 | echo "" >> $1 28 | fi 29 | -------------------------------------------------------------------------------- /doc/usage_examples.md: -------------------------------------------------------------------------------- 1 | # Usage example 2 | 3 | ## Loading a plugin 4 | The example below does the following: 5 | 6 | - instantiates a plugin object by reading a `.so` (in Windows it would be a `.dll`) vst plugin file. 7 | - checks some plugin attributes 8 | 9 | ```python 10 | from cython_vst_loader.vst_host import VstHost 11 | from cython_vst_loader.vst_plugin import VstPlugin 12 | from cython_vst_loader.vst_loader_wrapper import allocate_float_buffer, get_float_buffer_as_list, free_buffer, \ 13 | allocate_double_buffer, get_double_buffer_as_list 14 | from cython_vst_loader.vst_event import VstNoteOnMidiEvent 15 | 16 | 17 | sample_rate = 44100 18 | buffer_size = 512 19 | 20 | host = VstHost(sample_rate, buffer_size) 21 | 22 | # Audio will be rendered into these buffers: 23 | right_output = allocate_float_buffer(buffer_size, 1) 24 | left_output = allocate_float_buffer(buffer_size, 1) 25 | 26 | # `right_output` and `left_output` are integers which are, in fact, 27 | # just pointers to float32 arrays cast to `int` 28 | 29 | # These buffers are not managed by Python, and, therefore, are not garbage collected. 30 | # use free_buffer to free up the memory 31 | 32 | plugin_path = "/usr/bin/vst/amsynth-vst.x86_64-linux.so" 33 | plugin = VstPlugin(plugin_path.encode('utf-8'), host) 34 | 35 | 36 | # now we can work with this object representing a plugin instance: 37 | assert(41 == plugin.get_num_parameters()) 38 | assert (b'amp_attack' == plugin.get_parameter_name(0)) 39 | assert (0.0 == plugin.get_parameter_value(0)) 40 | ``` 41 | 42 | ## Doing something useful with the plugin 43 | 44 | The following example performs one cycle of rendering, involving both sending events and requesting to render audio. 45 | ```python 46 | # 3: delta frames, at which frame(sample) in the current buffer the event occurs 47 | # 85: the note number (85 = C# in the 7th octave) 48 | # 100: velocity (0..128) 49 | # 1: midi channel 50 | event = VstNoteOnMidiEvent(3, 85, 100, 1) 51 | 52 | plugin.process_events([event]) 53 | plugin.process_replacing([], [right_output, left_output], buffer_size) 54 | # at this point, the buffers are expected to have some sound of the C# playing 55 | ``` 56 | 57 | ### limit on number of processed event for one buffer 58 | 59 | Currently, the maximum number of events processed per one buffer is 1024. 60 | This seems like a reasonable assumption for most use cases. 61 | 62 | A PR is welcome if you see an elegant way to lift this limitation. 63 | (see the change set in https://github.com/hq9000/cython-vst-loader/pull/8 for reference) 64 | 65 | 66 | ## Freeing up buffers 67 | 68 | ```python 69 | # when we are done, we free up the buffers 70 | free_buffer(left_output) 71 | free_buffer(right_output) 72 | ``` 73 | 74 | ## Using numpy arrays as buffers 75 | 76 | Although this library does not depend on numpy, you can use numpy arrays as buffers like so: 77 | 78 | ```python 79 | import numpy as np 80 | 81 | def numpy_array_to_pointer(numpy_array: np.ndarray) -> int: 82 | if numpy_array.ndim != 1: 83 | raise Exception('expected a 1d numpy array here') 84 | pointer, _ = numpy_array.__array_interface__['data'] 85 | return pointer 86 | ``` 87 | 88 | the resulting value can be supplied to `VstPlugin.process_replacing` as a buffer 89 | 90 | **note:** if buffers are created this way, they are managed by numpy and, therefore, 91 | should not be freed manually 92 | 93 | ## Plugins used for testing 94 | 95 | The following open source vst plugins were compiled for linux x86_64 and put into `tests/test_plugins` directory: 96 | - https://github.com/amsynth/amsynth 97 | - https://github.com/michaelwillis/dragonfly-reverb -------------------------------------------------------------------------------- /inspect_platform.py: -------------------------------------------------------------------------------- 1 | import os 2 | from sys import platform 3 | 4 | print("sys.platform: " + platform) 5 | print("os.name: " + os.name) 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==19.3.0 2 | Cython==0.29.19 3 | importlib-metadata==1.6.0 4 | more-itertools==8.3.0 5 | packaging==20.4 6 | pluggy==0.13.1 7 | py==1.8.1 8 | pyparsing==2.4.7 9 | pytest==5.4.2 10 | six==1.15.0 11 | wcwidth==0.1.9 12 | zipp==3.1.0 13 | parameterized==0.8.1 14 | flake8==3.9.0 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # from distutils.core import setup 2 | import setuptools 3 | from pathlib import Path 4 | import os 5 | import os.path 6 | 7 | from setuptools import Extension 8 | 9 | USE_CYTHON = True 10 | 11 | try: 12 | # noinspection PyUnresolvedReferences 13 | from Cython.Build import cythonize 14 | except ImportError: 15 | USE_CYTHON = False 16 | 17 | this_directory = Path(__file__).parents[0] 18 | 19 | if not os.path.exists(this_directory.as_posix() + "/build/vstsdk/pluginterfaces"): 20 | os.system("make") 21 | 22 | include_paths = [ 23 | this_directory.as_posix() + "/build/vstsdk/pluginterfaces/vst2.x", 24 | this_directory.as_posix() + "/cython_vst_loader/include" 25 | ] 26 | 27 | 28 | def is_windows(): 29 | return os.name == 'nt' 30 | 31 | 32 | if USE_CYTHON: 33 | ext_modules = cythonize( 34 | 'cython_vst_loader/vst_loader_wrapper.pyx', 35 | compiler_directives={'language_level': "3"} 36 | ) 37 | else: 38 | ext_modules = [ 39 | Extension("cython_vst_loader.vst_loader_wrapper", ["cython_vst_loader/vst_loader_wrapper.c"]), 40 | ] 41 | 42 | # workaround for https://github.com/cython/cython/issues/1480 43 | for module in ext_modules: 44 | module.include_dirs = include_paths 45 | 46 | if not is_windows(): 47 | module.extra_compile_args = [ 48 | "-Wno-unused-function" 49 | ] 50 | 51 | with open(str(this_directory) + '/README.md', encoding='utf-8') as f: 52 | long_description = f.read() 53 | 54 | setuptools.setup( 55 | ext_modules=ext_modules, 56 | name='cython_vst_loader', 57 | packages=setuptools.find_packages(exclude=("tests",)), 58 | use_scm_version={ 59 | "root": ".", 60 | "relative_to": __file__, 61 | "local_scheme": "node-and-timestamp" 62 | }, 63 | setup_requires=['setuptools_scm'], 64 | license='MIT', 65 | description='a cython-based loader for VST audio plugins providing a clean python object-oriented interface', 66 | long_description=long_description, 67 | long_description_content_type='text/markdown', 68 | author='Sergey Grechin', # Type in your name 69 | author_email='grechin.sergey@gmail.com', 70 | url='https://github.com/hq9000/cython-vst-loader', 71 | keywords=['vst', 'plugin', 'cython'], 72 | classifiers=[ 73 | 'Development Status :: 4 - Beta', 74 | 'Intended Audience :: Developers', 75 | 'License :: OSI Approved :: MIT License', 76 | 'Programming Language :: Python :: 3', 77 | 'Programming Language :: Python :: 3.7', 78 | 'Programming Language :: Python :: 3.8', 79 | 'Programming Language :: Python :: 3.9' 80 | ], 81 | ) 82 | -------------------------------------------------------------------------------- /tests/linting_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import unittest 4 | 5 | 6 | class LintingTest(unittest.TestCase): 7 | def test_flake8_main_code_and_tests(self): 8 | this_dir = os.path.dirname(os.path.realpath(__file__)) 9 | 10 | checks_to_ignore = [ 11 | 'E501', 12 | 'W503' 13 | ] 14 | 15 | cmd_line_parts = [ 16 | "flake8", 17 | this_dir + "/../cython_vst_loader", 18 | this_dir + "/../tests", 19 | "--count", 20 | f'--ignore={",".join(checks_to_ignore)}', 21 | '--show-source', 22 | '--statistics' 23 | ] 24 | 25 | result = subprocess.run(cmd_line_parts) 26 | self.assertEqual(0, result.returncode, str(result.stdout) + str(result.stderr)) 27 | 28 | 29 | if __name__ == '__main__': 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /tests/test_buffers.py: -------------------------------------------------------------------------------- 1 | # noinspection PyUnresolvedReferences 2 | import unittest 3 | 4 | from cython_vst_loader.vst_loader_wrapper import allocate_float_buffer, get_float_buffer_as_list, \ 5 | free_buffer, \ 6 | allocate_double_buffer, get_double_buffer_as_list 7 | 8 | 9 | class TestBuffers(unittest.TestCase): 10 | 11 | def test_float_buffer(self): 12 | pointer = allocate_float_buffer(10, 12.345) 13 | assert (pointer > 1000) # something like a pointer 14 | list_object = get_float_buffer_as_list(pointer, 10) 15 | assert (isinstance(list_object, list)) 16 | assert (len(list_object) == 10) 17 | for element in list_object: 18 | assert (self.roughly_equals(element, 12.345)) 19 | free_buffer(pointer) 20 | 21 | def test_double_buffer(self): 22 | pointer = allocate_double_buffer(10, 12.345) 23 | assert (pointer > 1000) # something like a pointer 24 | list_object = get_double_buffer_as_list(pointer, 10) 25 | assert (isinstance(list_object, list)) 26 | assert (len(list_object) == 10) 27 | for element in list_object: 28 | assert (self.roughly_equals(element, 12.345)) 29 | free_buffer(pointer) 30 | 31 | def roughly_equals(self, a: float, b: float) -> bool: 32 | tolerance: float = 0.00001 33 | return abs(a - b) < tolerance 34 | -------------------------------------------------------------------------------- /tests/test_plugins/.gitignore: -------------------------------------------------------------------------------- 1 | non_distributable -------------------------------------------------------------------------------- /tests/test_plugins/DragonflyPlateReverb-vst.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/tests/test_plugins/DragonflyPlateReverb-vst.dll -------------------------------------------------------------------------------- /tests/test_plugins/DragonflyRoomReverb-vst.x86_64-linux.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/tests/test_plugins/DragonflyRoomReverb-vst.x86_64-linux.so -------------------------------------------------------------------------------- /tests/test_plugins/OB-Xd.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/tests/test_plugins/OB-Xd.dll -------------------------------------------------------------------------------- /tests/test_plugins/Synth1_vst.x86_64-windows.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/tests/test_plugins/Synth1_vst.x86_64-windows.dll -------------------------------------------------------------------------------- /tests/test_plugins/TAL-Elek7ro-II.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/tests/test_plugins/TAL-Elek7ro-II.dll -------------------------------------------------------------------------------- /tests/test_plugins/TAL-NoiseMaker-64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/tests/test_plugins/TAL-NoiseMaker-64.dll -------------------------------------------------------------------------------- /tests/test_plugins/TAL-Reverb-2-64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/tests/test_plugins/TAL-Reverb-2-64.dll -------------------------------------------------------------------------------- /tests/test_plugins/Tunefish4.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/tests/test_plugins/Tunefish4.dll -------------------------------------------------------------------------------- /tests/test_plugins/TyrellN6(x64).dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/tests/test_plugins/TyrellN6(x64).dll -------------------------------------------------------------------------------- /tests/test_plugins/amsynth-vst.x86_64-linux.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq9000/cython-vst-loader/f24f2090a836b3cb739daf403f0ca5135f44d233/tests/test_plugins/amsynth-vst.x86_64-linux.so -------------------------------------------------------------------------------- /tests/test_vst_plugin_linux.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # noinspection PyUnresolvedReferences 4 | import unittest 5 | from sys import platform 6 | 7 | from cython_vst_loader.vst_loader_wrapper import allocate_float_buffer, get_float_buffer_as_list, free_buffer 8 | from cython_vst_loader.vst_event import VstNoteOnMidiEvent 9 | from cython_vst_loader.vst_host import VstHost 10 | from cython_vst_loader.vst_plugin import VstPlugin 11 | 12 | 13 | @unittest.skipIf(platform != 'linux', 14 | 'this test case is supposed to be run on linux only, and this platform is ' + str(platform)) 15 | class TestInLinux(unittest.TestCase): 16 | 17 | def test_with_amsynth_general(self): 18 | host = VstHost(44100, 512) 19 | 20 | this_dir: str = os.path.dirname(os.path.realpath(__file__)) 21 | plugin_path: str = this_dir + "/test_plugins/amsynth-vst.x86_64-linux.so" 22 | plugin = VstPlugin(plugin_path.encode('utf-8'), host) 23 | 24 | assert (41 == plugin.get_num_parameters()) 25 | 26 | assert (0 == plugin.get_num_input_channels()) # it's a synth, that's why 27 | assert (plugin.is_synth()) 28 | assert (plugin.allows_double_precision() is False) 29 | 30 | assert (b'amp_attack' == plugin.get_parameter_name(0)) 31 | assert (0.0 == plugin.get_parameter_value(0)) 32 | plugin.set_parameter_value(0, 0.1) 33 | assert (0.09 < plugin.get_parameter_value(0) < 0.11) # to account for float imprecision 34 | 35 | right_output = allocate_float_buffer(512, 1) 36 | left_output = allocate_float_buffer(512, 1) 37 | 38 | # this is a relaxed check that these actually are something like valid pointers 39 | assert (right_output > 10000) 40 | assert (left_output > 10000) 41 | 42 | plugin.process_replacing([], [right_output, left_output], 512) 43 | 44 | right_output_as_list = get_float_buffer_as_list(right_output, 512) 45 | left_output_as_list = get_float_buffer_as_list(left_output, 512) 46 | 47 | for i in range(0, 512): 48 | assert (right_output_as_list[i] == 0.0) 49 | assert (left_output_as_list[i] == 0.0) 50 | 51 | # now let's play a note 52 | event_note_on = VstNoteOnMidiEvent(3, 85, 100, 1) 53 | event_note_off = VstNoteOnMidiEvent(4, 85, 0, 1) 54 | 55 | event_note_on_next = VstNoteOnMidiEvent(86, 85, 100, 1) 56 | 57 | faced_non_zero: bool = False 58 | 59 | # trying 10 times to get some noise at sample 6 60 | # this is due to the non-deterministic behaviour of this synth 61 | for _i in range(1, 100): 62 | 63 | plugin.process_events([event_note_on, event_note_off, event_note_on_next]) 64 | plugin.process_replacing([], [right_output, left_output], 512) 65 | 66 | right_output_as_list = get_float_buffer_as_list(right_output, 512) 67 | left_output_as_list = get_float_buffer_as_list(left_output, 512) 68 | 69 | # http://i.imgur.com/DNGyvYq.png 70 | for i in range(0, 4): 71 | assert (0.0 == right_output_as_list[i]) 72 | assert (0.0 == left_output_as_list[i]) 73 | 74 | if 0.0 != right_output_as_list[6]: 75 | faced_non_zero = True 76 | break 77 | 78 | assert (faced_non_zero is True) 79 | 80 | # since we have shut the note up almost immediately, in the end of the buffer 81 | # (let's say the 81-82th samples) should be 0 again 82 | assert (0.0 == right_output_as_list[81]) 83 | assert (0.0 == left_output_as_list[82]) 84 | 85 | # however, then next note has already started on the 90th 86 | assert (0.0 != right_output_as_list[95]) 87 | assert (0.0 != left_output_as_list[96]) 88 | 89 | free_buffer(right_output) 90 | free_buffer(left_output) 91 | 92 | def test_amsynth_many_events_to_process(self): 93 | host = VstHost(44100, 512) 94 | 95 | this_dir: str = os.path.dirname(os.path.realpath(__file__)) 96 | plugin_path: str = this_dir + "/test_plugins/amsynth-vst.x86_64-linux.so" 97 | plugin = VstPlugin(plugin_path.encode('utf-8'), host) 98 | 99 | event_nums = [1, 3, 15, 16, 32, 512, 1023, 1024] 100 | 101 | for num in event_nums: 102 | events = [VstNoteOnMidiEvent(3, 85, 100, 1)] * num 103 | plugin.process_events(events) 104 | 105 | right_output = allocate_float_buffer(512, 1) 106 | left_output = allocate_float_buffer(512, 1) 107 | 108 | right_output_as_list = get_float_buffer_as_list(right_output, 512) 109 | left_output_as_list = get_float_buffer_as_list(left_output, 512) 110 | assert (1.0 == right_output_as_list[95]) 111 | assert (1.0 == left_output_as_list[96]) 112 | plugin.process_replacing([], [right_output, left_output], 512) 113 | 114 | right_output_as_list = get_float_buffer_as_list(right_output, 512) 115 | left_output_as_list = get_float_buffer_as_list(left_output, 512) 116 | 117 | assert (1.0 != right_output_as_list[95]) 118 | assert (1.0 != left_output_as_list[96]) 119 | 120 | free_buffer(right_output) 121 | free_buffer(left_output) 122 | 123 | def test_amsynth_limitation_on_num_events(self): 124 | host = VstHost(44100, 512) 125 | 126 | this_dir: str = os.path.dirname(os.path.realpath(__file__)) 127 | plugin_path: str = this_dir + "/test_plugins/amsynth-vst.x86_64-linux.so" 128 | plugin = VstPlugin(plugin_path.encode('utf-8'), host) 129 | 130 | events = [VstNoteOnMidiEvent(3, 85, 100, 1)] * 1025 131 | try: 132 | plugin.process_events(events) 133 | raise ValueError('this line should not have been reached. Exception should have been thrown before') 134 | except ValueError as e: 135 | assert (str(e).endswith('(error: edaa3dff)')) 136 | 137 | def test_with_dragonfly_reverb(self): 138 | buffer_length: int = 1024 139 | 140 | host = VstHost(44100, buffer_length) 141 | 142 | this_dir: str = os.path.dirname(os.path.realpath(__file__)) 143 | plugin_path: str = this_dir + "/test_plugins/DragonflyRoomReverb-vst.x86_64-linux.so" 144 | plugin = VstPlugin(plugin_path.encode('utf-8'), host) 145 | 146 | assert (plugin.is_synth() is False) 147 | assert (plugin.allows_double_precision() is False) 148 | assert (17 == plugin.get_num_parameters()) 149 | assert (2 == plugin.get_num_input_channels()) 150 | assert (2 == plugin.get_num_input_channels()) 151 | assert (b'Dry Level' == plugin.get_parameter_name(0)) 152 | plugin.set_parameter_value(0, 0.123123) 153 | assert (0.123 < plugin.get_parameter_value(0) < 0.124) 154 | 155 | left_input = allocate_float_buffer(buffer_length, 1) 156 | right_input = allocate_float_buffer(buffer_length, 1) 157 | 158 | left_output = allocate_float_buffer(buffer_length, 0) 159 | right_output = allocate_float_buffer(buffer_length, 0) 160 | 161 | plugin.process_replacing([left_input, right_input], [left_output, right_output], buffer_length) 162 | 163 | left_output_as_list = get_float_buffer_as_list(left_output, buffer_length) 164 | 165 | # this is roughly input level 1 multiplied by dry level (0.123) 166 | assert (0.123 < left_output_as_list[2] < 0.124) 167 | -------------------------------------------------------------------------------- /tests/test_vst_plugin_win.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from cython_vst_loader.vst_event import VstNoteOnMidiEvent 5 | from cython_vst_loader.vst_host import VstHost 6 | from cython_vst_loader.vst_loader_wrapper import allocate_float_buffer, get_float_buffer_as_list, free_buffer, \ 7 | allocate_double_buffer 8 | from cython_vst_loader.vst_plugin import VstPlugin 9 | from parameterized import parameterized 10 | 11 | 12 | @unittest.skipIf(os.name != 'nt', 'this test case is supposed to be run on windows') 13 | class TestPluginsWinTestCase(unittest.TestCase): 14 | 15 | def test_obxd_basic(self): 16 | host = VstHost(44100, 512) 17 | 18 | this_dir: str = os.path.dirname(os.path.realpath(__file__)) 19 | plugin_path: str = this_dir + "/test_plugins/OB-Xd.dll" 20 | plugin = VstPlugin(plugin_path.encode('utf-8'), host) 21 | 22 | # weirdly, this one returns 255 as num parameters, tbh dunno if its bug of a loader or not 23 | self.assertEqual(80, plugin.get_num_parameters()) 24 | self.assertEqual(b'Unison', plugin.get_parameter_name(14)) 25 | self.assertEqual(0, plugin.get_num_input_channels()) # I guess it's because it's a synth 26 | self.assertEqual(1.0, plugin.get_parameter_value(3)) 27 | 28 | plugin.set_parameter_value(3, 0.1) 29 | self.assertTrue(0.095 < plugin.get_parameter_value(3) < 0.11) 30 | self.assertTrue(plugin.is_synth()) 31 | 32 | right_output = allocate_float_buffer(512, 1) 33 | left_output = allocate_float_buffer(512, 1) 34 | 35 | right_output_as_list = get_float_buffer_as_list(right_output, 512) 36 | left_output_as_list = get_float_buffer_as_list(left_output, 512) 37 | 38 | for i in range(0, 512): 39 | assert (right_output_as_list[i] == 1.0) 40 | assert (left_output_as_list[i] == 1.0) 41 | 42 | plugin.process_replacing([], [right_output, left_output], 512) 43 | 44 | right_output_as_list = get_float_buffer_as_list(right_output, 512) 45 | left_output_as_list = get_float_buffer_as_list(left_output, 512) 46 | 47 | abs_sum = 0 48 | for i in range(0, 512): 49 | assert (abs(right_output_as_list[i]) < 0.0001) # this plugin seems to be adding some noise 50 | assert (abs(left_output_as_list[i]) < 0.0001) 51 | abs_sum += abs(right_output_as_list[i]) 52 | 53 | self.assertLess(abs_sum, 0.1) 54 | 55 | event_note_on = VstNoteOnMidiEvent(3, 85, 100, 1) 56 | event_note_off = VstNoteOnMidiEvent(512, 85, 0, 1) 57 | 58 | plugin.process_events([event_note_on, event_note_off]) 59 | plugin.process_replacing([], [right_output, left_output], 512) 60 | 61 | right_output_as_list = get_float_buffer_as_list(right_output, 512) 62 | left_output_as_list = get_float_buffer_as_list(left_output, 512) 63 | 64 | abs_sum = 0 65 | for i in range(512): 66 | abs_sum += abs(right_output_as_list[i]) 67 | 68 | self.assertGreater(abs_sum, 1) 69 | 70 | free_buffer(right_output) 71 | free_buffer(left_output) 72 | 73 | @parameterized.expand([ 74 | ('TAL-Elek7ro-II.dll', False, 512, 107), 75 | ('TAL-Elek7ro-II.dll', False, 256, 107), 76 | ('TAL-Elek7ro-II.dll', False, 1024, 107), 77 | ('OB-Xd.dll', False, 512, 80), 78 | ('OB-Xd.dll', False, 1024, 80), 79 | ('OB-Xd.dll', False, 256, 80), 80 | ('TAL-NoiseMaker-64.dll', False, 256, 92), 81 | ('TAL-NoiseMaker-64.dll', False, 1024, 92), 82 | ('Tunefish4.dll', False, 512, 112), 83 | ('Tunefish4.dll', False, 1024, 112), 84 | ('Tunefish4.dll', False, 256, 112) 85 | ]) 86 | def test_synth_many_events_to_process(self, relative_path_to_synth_plugin: str, double_processing: bool, 87 | buffer_size: int, expected_num_parameters): 88 | host = VstHost(44100, 512) 89 | 90 | this_dir: str = os.path.dirname(os.path.realpath(__file__)) 91 | plugin_path: str = this_dir + '/test_plugins/' + relative_path_to_synth_plugin 92 | plugin = VstPlugin(plugin_path.encode('utf-8'), host) 93 | 94 | self.assertEqual(expected_num_parameters, plugin.get_num_parameters()) 95 | 96 | event_nums = [0, 0, 3, 0, 15, 16, 16, 16, 16, 17, 32, 512, 1023, 1021] 97 | 98 | if double_processing: 99 | right_input = allocate_double_buffer(buffer_size, 1) 100 | left_input = allocate_double_buffer(buffer_size, 1) 101 | right_output = allocate_double_buffer(buffer_size, 1) 102 | left_output = allocate_double_buffer(buffer_size, 1) 103 | else: 104 | right_input = allocate_float_buffer(buffer_size, 1) 105 | left_input = allocate_float_buffer(buffer_size, 1) 106 | right_output = allocate_float_buffer(buffer_size, 1) 107 | left_output = allocate_float_buffer(buffer_size, 1) 108 | 109 | for i in range(60): 110 | for num in event_nums: 111 | events = [] 112 | for idx in range(num): 113 | events.append(VstNoteOnMidiEvent(3 + num, 85, 100, 1)) 114 | 115 | plugin.process_events(events) 116 | if double_processing: 117 | plugin.process_double_replacing([right_input, left_input], [right_output, left_output], buffer_size) 118 | else: 119 | plugin.process_replacing([right_input, left_input], [right_output, left_output], buffer_size) 120 | 121 | free_buffer(right_output) 122 | free_buffer(left_output) 123 | free_buffer(right_input) 124 | free_buffer(left_input) 125 | 126 | @parameterized.expand([ 127 | ('DragonflyPlateReverb-vst.dll', 9), 128 | ('TAL-Reverb-2-64.dll', 13), 129 | ]) 130 | def test_with_dragonfly_reverb(self, relative_plugin_path: str, expected_number_of_parameters: int): 131 | buffer_length: int = 1024 132 | 133 | host = VstHost(44100, buffer_length) 134 | 135 | this_dir: str = os.path.dirname(os.path.realpath(__file__)) 136 | 137 | plugin_path: str = this_dir + "/test_plugins/" + relative_plugin_path 138 | 139 | plugin = VstPlugin(plugin_path.encode('utf-8'), host) 140 | 141 | assert (plugin.is_synth() is False) 142 | assert (plugin.allows_double_precision() is False) 143 | assert (expected_number_of_parameters == plugin.get_num_parameters()) 144 | assert (2 == plugin.get_num_input_channels()) 145 | assert (2 == plugin.get_num_output_channels()) 146 | 147 | plugin.set_parameter_value(0, 0.123123) 148 | assert (0.123 < plugin.get_parameter_value(0) < 0.124) 149 | 150 | left_input = allocate_float_buffer(buffer_length, 1) 151 | right_input = allocate_float_buffer(buffer_length, 1) 152 | 153 | left_output = allocate_float_buffer(buffer_length, 0) 154 | right_output = allocate_float_buffer(buffer_length, 0) 155 | 156 | plugin.process_replacing([left_input, right_input], [left_output, right_output], buffer_length) 157 | --------------------------------------------------------------------------------