├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── conftest.py ├── examples └── simple_host.py ├── pyvst ├── __init__.py ├── host.py ├── midi.py ├── vstplugin.py └── vstwrap.py ├── scripts └── print_plugin.py ├── setup.py └── tests ├── test_example.py ├── test_host.py ├── test_midi.py └── test_plugin.py /.gitignore: -------------------------------------------------------------------------------- 1 | # vim 2 | *~ 3 | *.swp 4 | *.swo 5 | 6 | # python 7 | *.pyc 8 | 9 | /.* 10 | /tmp 11 | /pyvst.egg-info 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Next Release 2 | 3 | # 0.5.0 4 | 5 | * A lof of minor tweaks (4d08b11) 6 | 7 | # 0.4.0 8 | 9 | * Capture the stdout/stderr of the plugin. (79d33bd) 10 | * Change the default play_note max_duration to 5 seconds. (79d33bd) 11 | 12 | # 0.3.0 13 | 14 | * Big fixes in SimpleHost + general cleaning. Also now `SimpleHost.play_note` as defaults for all 15 | arguments. (1fd61e9) 16 | 17 | # 0.2.0 18 | 19 | * Have an option for the `play_note` of the `SimpleHost` to stop playing 20 | automatically (7c85d24) 21 | 22 | # 0.1.0 23 | 24 | First release! 25 | 26 | * Basic code to load a VST 27 | * SimpleHost to load a vst and use it to play a note 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | RUN apt-get update \ 4 | && DEBIAN_FRONTEND=noninteractive apt-get install -y -q \ 5 | python3 \ 6 | python3-pip \ 7 | libx11-dev \ 8 | libxext-dev \ 9 | libxinerama-dev \ 10 | libasound-dev \ 11 | libfreetype6 \ 12 | # For TyrellN6 13 | libglib2.0 \ 14 | libcairo2 \ 15 | # amsynth 16 | libgtk2.0-0 \ 17 | && rm -rf /var/lib/apt/lists/* 18 | 19 | WORKDIR /workdir/pyvst 20 | 21 | COPY setup.py /workdir/pyvst/setup.py 22 | 23 | RUN pip3 install -U pip 24 | # Installing with -e, effectively only writing a simlink, assuming the code will be mounted. 25 | RUN pip3 install -e /workdir/pyvst[dev] 26 | # Putting this one here because not really a dependency 27 | RUN pip3 install ipython 28 | 29 | ENV HOME /workdir/pyvst 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Simon Lemieux 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build run 2 | 3 | build: 4 | docker build . -t pyvst 5 | 6 | # To be run from the repo! 7 | run: 8 | docker run -it --rm \ 9 | --volume `pwd`:/workdir/pyvst/ \ 10 | --user `id -u`:`id -g` \ 11 | pyvst bash 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | My attempt at hosting VSTs using `ctypes`. 2 | 3 | 4 | # Running in Docker 5 | 6 | make build 7 | make run 8 | pytest tests --verbose 9 | 10 | 11 | Check out the example in [`examples/simple_host.py`][1]. 12 | 13 | [1]: examples/simple_host.py 14 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyvst import SimpleHost 4 | 5 | 6 | def _find_test_plugins(): 7 | """ 8 | One plugin path per line in .test_plugin_path.txt 9 | """ 10 | with open('.test_plugin_path.txt') as f: 11 | path = f.read().strip() 12 | 13 | lines = path.split('\n') 14 | lines = [x.strip() for x in lines] 15 | lines = [x for x in lines if not x.startswith('#')] 16 | return lines 17 | 18 | 19 | _VST_PLUGINS = _find_test_plugins() 20 | 21 | 22 | @pytest.fixture(params=_VST_PLUGINS) 23 | def vst(request): 24 | return request.param 25 | 26 | 27 | @pytest.fixture() 28 | def host(vst): 29 | """SimpleHost containing a loaded vst.""" 30 | host = SimpleHost(vst) 31 | return host 32 | -------------------------------------------------------------------------------- /examples/simple_host.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from pyvst import SimpleHost 3 | 4 | 5 | def _print_params(vst, max_params=10): 6 | """Prints the parameters of a VST with its current value.""" 7 | for i in range(min(vst.num_params, max_params)): 8 | print('{}: {}'.format( 9 | vst.get_param_name(i), 10 | vst.get_param_value(i), 11 | )) 12 | 13 | 14 | def main(vst_filename): 15 | host = SimpleHost(vst_filename, sample_rate=48000.) 16 | _print_params(host.vst) 17 | 18 | sound = host.play_note(note=64, note_duration=1.) 19 | print(sound) 20 | print(sound.shape) 21 | 22 | host.vst.set_param_value(index=0, value=1.) 23 | host.vst.set_param_value(index=1, value=0.5) 24 | 25 | _print_params(host.vst) 26 | 27 | sound = host.play_note(note=64, note_duration=1.) 28 | print(sound) 29 | print(sound.shape) 30 | 31 | 32 | if __name__ == '__main__': 33 | parser = argparse.ArgumentParser() 34 | parser.add_argument('vst', help='path to .so file') 35 | args = parser.parse_args() 36 | 37 | main(args.vst) 38 | -------------------------------------------------------------------------------- /pyvst/__init__.py: -------------------------------------------------------------------------------- 1 | from .vstplugin import VstPlugin 2 | from .host import SimpleHost 3 | -------------------------------------------------------------------------------- /pyvst/host.py: -------------------------------------------------------------------------------- 1 | from ctypes import addressof, create_string_buffer 2 | from warnings import warn 3 | 4 | import numpy as np 5 | 6 | from pyvst import VstPlugin 7 | from pyvst.vstwrap import VstTimeInfoFlags, VstTimeInfo, AudioMasterOpcodes 8 | from pyvst.midi import midi_note_event, wrap_vst_events 9 | 10 | 11 | # Class inspired from MrsWatson's audioClock 12 | class Transport: 13 | """ 14 | Class responsible to know where we are in the "song". 15 | It knows the sample_rate and tempo and so it can convert the position in frames/seconds/beats. 16 | It also notes when it's changing from play/stop, etc, which can be asked by VSTs. 17 | """ 18 | def __init__(self, sample_rate, tempo=120.): 19 | self._sample_rate = sample_rate 20 | self.tempo = tempo 21 | 22 | # position in frames 23 | self._position = 0. 24 | self.has_changed = False 25 | self.is_playing = False 26 | 27 | def step(self, num_frames): 28 | self._position += num_frames 29 | if not self.is_playing: 30 | self.is_playing = True 31 | self.has_changed = True 32 | else: 33 | self.has_changed = False 34 | 35 | def stop(self): 36 | if self.is_playing: 37 | self.is_playing = False 38 | self.has_changed = True 39 | else: 40 | self.has_changed = False 41 | 42 | def reset(self): 43 | self.stop() 44 | self._position = 0. 45 | 46 | def get_position(self, unit='frame'): 47 | if unit == 'frame': 48 | return self._position 49 | elif unit == 'beat': # same as a quarter 50 | return self.tempo * self._position / 60. / self._sample_rate 51 | elif unit == 'second': 52 | return self._position / self._sample_rate 53 | else: 54 | raise ValueError('Unknown unit "{}"'.format(unit)) 55 | 56 | 57 | class SimpleHost: 58 | """Simple host that holds a single (synth) vst.""" 59 | 60 | _product_string = create_string_buffer(b'pyvst SimpleHost') 61 | 62 | def __init__(self, vst_filename=None, sample_rate=44100., tempo=120., block_size=512): 63 | self.sample_rate = sample_rate 64 | self.transport = Transport(sample_rate, tempo) 65 | self.block_size = block_size 66 | 67 | def callback(*args): 68 | return self._audio_master_callback(*args) 69 | 70 | self._callback = callback 71 | self._vst = None 72 | self._vst_path = None 73 | 74 | if vst_filename is not None: 75 | self.load_vst(vst_filename) 76 | 77 | @property 78 | def vst(self): 79 | if self._vst is None: 80 | raise RuntimeError('You must first load a vst using `self.load_vst`.') 81 | return self._vst 82 | 83 | def reload_vst(self): 84 | params = [self.vst.get_param_value(i) for i in range(self.vst.num_params)] 85 | self.vst.suspend() 86 | self.vst.close() 87 | del self._vst 88 | 89 | self.load_vst(self._path_to_so_file) 90 | for i, p in enumerate(params): 91 | self.vst.set_param_value(i, p) 92 | 93 | def load_vst(self, path_to_so_file): 94 | """ 95 | Loads a vst. If there was already a vst loaded, we will release it. 96 | 97 | :param path_to_so_file: Path to the .so file to use as a plugin. 98 | """ 99 | self._vst = VstPlugin(path_to_so_file, self._callback) 100 | 101 | # Not sure I need this but I've seen it in other hosts 102 | self.vst.open() 103 | 104 | if not self._vst.is_synth: 105 | raise RuntimeError('Your VST must be a synth!') 106 | 107 | self.vst.set_sample_rate(self.sample_rate) 108 | self.vst.set_block_size(self.block_size) 109 | self.vst.resume() 110 | 111 | # Warm up the VST by playing a quick note. It has fixed some issues for TyrellN6 where 112 | # otherwise the first note is funny. 113 | self._path_to_so_file = path_to_so_file 114 | self.play_note(note=64, min_duration=.1, max_duration=.1, note_duration=.1, velocity=127, 115 | reload=False) 116 | 117 | def play_note(self, note=64, note_duration=.5, velocity=100, max_duration=5., 118 | min_duration=0.01, volume_threshold=0.000002, reload=False): 119 | """ 120 | :param note_duration: Duration between the note on and note off midi events, in seconds. 121 | 122 | The audio will then last between `min_duration` and `max_duration`, stopping when 123 | sqrt(mean(signal ** 2)) falls under `volume_threshold` for a single buffer. For those 124 | arguments, `None` means they are ignored. 125 | 126 | :param reload: Will delete and reload the vst after having playing the note. It's an 127 | extreme way of making sure the internal state of the VST is reset. When False, we 128 | simply suspend() and resume() the VST (which should be enough in most cases). 129 | """ 130 | if max_duration is not None and max_duration < note_duration: 131 | raise ValueError('max_duration ({}) is smaller than the midi note_duration ({})' 132 | .format(max_duration, note_duration)) 133 | 134 | if min_duration is not None and max_duration is not None and max_duration < min_duration: 135 | raise ValueError('max_duration ({}) is smaller than min_duration ({})' 136 | .format(max_duration, min_duration)) 137 | 138 | # Call this here to fail fast in case the VST has not been loaded 139 | self.vst 140 | 141 | # Convert the durations from seconds to frames 142 | min_duration = round(min_duration * self.sample_rate) 143 | max_duration = round(max_duration * self.sample_rate) 144 | 145 | note_on = midi_note_event(note, velocity) 146 | 147 | # nb of frames before the note_off events 148 | noteoff_is_in = round(note_duration * self.sample_rate) 149 | 150 | outputs = [] 151 | 152 | self.transport.reset() 153 | 154 | # note_on is at time 0 anyway so we can do it before the loop 155 | self.vst.process_events(wrap_vst_events([note_on])) 156 | while True: 157 | if max_duration is not None and self.transport.get_position() > max_duration: 158 | break 159 | 160 | # If it's time for the note off 161 | if 0 <= noteoff_is_in < self.block_size: 162 | note_off = midi_note_event(note, 0, kind='note_off', delta_frames=noteoff_is_in) 163 | self.vst.process_events(wrap_vst_events([note_off])) 164 | 165 | output = self.vst.process(input=None, sample_frames=self.block_size) 166 | outputs.append(output) 167 | 168 | # If we are past the min_position, and if we have a volume_threshold, then we see if 169 | # we have enough volume to continue. 170 | if self.transport.get_position() > min_duration and volume_threshold: 171 | rms = np.sqrt((output ** 2).mean()) 172 | if rms < volume_threshold: 173 | break 174 | 175 | # We move transport in the future 176 | self.transport.step(self.block_size) 177 | # Which means the "noteoff is in" one block_size sooner 178 | noteoff_is_in -= self.block_size 179 | 180 | # Concatenate all the output buffers 181 | outputs = np.hstack(outputs) 182 | 183 | # Cut the extra of the last buffer if need be, to respect the `max_duration`. 184 | if max_duration is not None: 185 | outputs = outputs[:, :max_duration] 186 | 187 | # Reset the plugin to clear its state 188 | if reload: 189 | self.reload_vst() 190 | else: 191 | self.vst.suspend() 192 | self.vst.resume() 193 | 194 | return outputs 195 | 196 | def _audio_master_callback(self, effect, opcode, index, value, ptr, opt): 197 | # Note that there are a lot of missing opcodes here, I basically add them as I see VST 198 | # asking for them... 199 | if opcode == AudioMasterOpcodes.audioMasterVersion: 200 | return 2400 201 | # Deprecated but some VSTs still ask for it 202 | elif opcode == AudioMasterOpcodes.audioMasterWantMidi: 203 | return 1 204 | elif opcode == AudioMasterOpcodes.audioMasterGetTime: 205 | # Very much inspired from MrsWatson 206 | sample_pos = self.transport.get_position() 207 | sample_rate = self.sample_rate 208 | flags = 0 209 | 210 | # Always return those 211 | if self.transport.has_changed: 212 | flags |= VstTimeInfoFlags.kVstTransportChanged 213 | if self.transport.is_playing: 214 | flags |= VstTimeInfoFlags.kVstTransportPlaying 215 | 216 | if value & VstTimeInfoFlags.kVstNanosValid: 217 | warn('Asked for VstTimeInfoFlags.kVstNanosValid but not supported yet') 218 | 219 | # Depending on the passed mask, we'll returned what was asked 220 | mask = value 221 | if mask & VstTimeInfoFlags.kVstPpqPosValid: 222 | ppq_pos = self.transport.get_position(unit='beat') 223 | flags |= VstTimeInfoFlags.kVstPpqPosValid 224 | 225 | if mask & VstTimeInfoFlags.kVstTempoValid: 226 | tempo = self.transport.tempo 227 | flags |= VstTimeInfoFlags.kVstTempoValid 228 | 229 | # TODO: Should we warn that we don't support the other ones? 230 | 231 | # Make sure it doesn't get garbage collected 232 | self._last_time_info = VstTimeInfo( 233 | sample_pos=sample_pos, 234 | sample_rate=sample_rate, 235 | ppq_pos=ppq_pos, 236 | tempo=tempo, 237 | flags=flags, 238 | ) 239 | return addressof(self._last_time_info) 240 | elif opcode == AudioMasterOpcodes.audioMasterGetProductString: 241 | return addressof(self._product_string) 242 | elif opcode == AudioMasterOpcodes.audioMasterIOChanged: 243 | return 0 244 | elif opcode == AudioMasterOpcodes.audioMasterGetCurrentProcessLevel: 245 | # This should mean "not supported by Host" 246 | return 0 247 | else: 248 | warn('Audio master call back opcode "{}" not supported yet'.format(opcode)) 249 | return 0 250 | -------------------------------------------------------------------------------- /pyvst/midi.py: -------------------------------------------------------------------------------- 1 | from ctypes import sizeof, cast, POINTER, pointer 2 | from .vstwrap import VstMidiEvent, VstEventTypes, VstEvent, get_vst_events_struct 3 | 4 | 5 | def _check_channel_valid(channel): 6 | if not (1 <= channel <= 16): 7 | raise ValueError('Invalid channel "{}". Must be in the [1, 16] range.' 8 | .format(channel)) 9 | 10 | 11 | def midi_note_as_bytes(note, velocity=100, kind='note_on', channel=1): 12 | """ 13 | :param channel: Midi channel (those are 1-indexed) 14 | """ 15 | if kind == 'note_on': 16 | kind_byte = b'\x90'[0] 17 | elif kind == 'note_off': 18 | kind_byte = b'\x80'[0] 19 | else: 20 | raise NotImplementedError('MIDI type {} not supported yet'.format(kind)) 21 | 22 | _check_channel_valid(channel) 23 | 24 | return bytes([ 25 | (channel - 1) | kind_byte, 26 | note, 27 | velocity 28 | ]) 29 | 30 | 31 | def midi_note_event(note, velocity=100, channel=1, kind='note_on', delta_frames=0): 32 | """ 33 | Generates a note (on or off) midi event (VstMidiEvent). 34 | 35 | :param note: midi note number 36 | :param velocity: 0-127 37 | :param channel: 1-16 38 | :param kind: "note_on" or "note_off" 39 | :delta_frames: In how many frames should the event happen. 40 | """ 41 | note_on = VstMidiEvent( 42 | type=VstEventTypes.kVstMidiType, 43 | byte_size=sizeof(VstMidiEvent), 44 | delta_frames=delta_frames, 45 | flags=0, 46 | note_length=0, 47 | note_offset=0, 48 | midi_data=midi_note_as_bytes(note, velocity, kind, channel), 49 | detune=0, 50 | note_off_velocity=127, 51 | ) 52 | return note_on 53 | 54 | 55 | def wrap_vst_events(midi_events): 56 | """Wraps a list VstMidiEvent into a VstEvents structure.""" 57 | p_midi_events = [pointer(x) for x in midi_events] 58 | p_midi_events = [cast(x, POINTER(VstEvent)) for x in p_midi_events] 59 | p_array = (POINTER(VstEvent) * len(midi_events)) 60 | Struct = get_vst_events_struct(len(midi_events)) 61 | events = Struct( 62 | num_events=len(midi_events), 63 | events=p_array(*p_midi_events) 64 | ) 65 | return events 66 | 67 | 68 | def all_sounds_off_event(channel=1): 69 | 70 | _check_channel_valid(channel) 71 | 72 | # See https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message 73 | midi_data = bytes([ 74 | (channel - 1) | b'\xb0'[0], 75 | 120, 76 | 0, 77 | ]) 78 | 79 | midi_event = VstMidiEvent( 80 | type=VstEventTypes.kVstMidiType, 81 | byte_size=sizeof(VstMidiEvent), 82 | delta_frames=0, 83 | flags=0, 84 | note_length=0, 85 | note_offset=0, 86 | midi_data=midi_data, 87 | detune=0, 88 | note_off_velocity=0, 89 | ) 90 | 91 | return midi_event 92 | -------------------------------------------------------------------------------- /pyvst/vstplugin.py: -------------------------------------------------------------------------------- 1 | from ctypes import (cdll, POINTER, c_double, 2 | c_void_p, c_int, c_float, c_int32, 3 | byref, string_at, create_string_buffer) 4 | from warnings import warn 5 | 6 | import numpy as np 7 | 8 | 9 | from .vstwrap import ( 10 | AudioMasterOpcodes, 11 | AEffect, 12 | AEffectOpcodes, 13 | AUDIO_MASTER_CALLBACK_TYPE, 14 | vst_int_ptr, 15 | VstPinProperties, 16 | VstParameterProperties, 17 | VstPlugCategory, 18 | VstAEffectFlags, 19 | ) 20 | 21 | 22 | # define kEffectMagic CCONST ('V', 's', 't', 'P') 23 | # or: MAGIC = int.from_bytes(b'VstP', 'big') 24 | MAGIC = 1450406992 25 | 26 | 27 | def _default_audio_master_callback(effect, opcode, *args): 28 | """Version naive audio master callback. This mimicks more than minimal host.""" 29 | if opcode == AudioMasterOpcodes.audioMasterVersion: 30 | return 2400 31 | return 0 32 | 33 | 34 | class VstPlugin: 35 | def __init__(self, filename, audio_master_callback=None): 36 | if audio_master_callback is None: 37 | audio_master_callback = _default_audio_master_callback 38 | self._lib = cdll.LoadLibrary(filename) 39 | self._lib.VSTPluginMain.argtypes = [AUDIO_MASTER_CALLBACK_TYPE] 40 | self._lib.VSTPluginMain.restype = POINTER(AEffect) 41 | 42 | self._effect = self._lib.VSTPluginMain(AUDIO_MASTER_CALLBACK_TYPE( 43 | audio_master_callback)).contents 44 | 45 | assert self._effect.magic == MAGIC 46 | 47 | if self.vst_version != 2400: 48 | warn('This plugin is not a VST2.4 plugin.') 49 | 50 | def open(self): 51 | self._dispatch(AEffectOpcodes.effOpen) 52 | 53 | def close(self): 54 | self._dispatch(AEffectOpcodes.effClose) 55 | 56 | def resume(self): 57 | self._dispatch(AEffectOpcodes.effMainsChanged, value=1) 58 | 59 | def suspend(self): 60 | self._dispatch(AEffectOpcodes.effMainsChanged, value=0) 61 | 62 | def _dispatch(self, opcode, index=0, value=0, ptr=None, opt=0.): 63 | if ptr is None: 64 | ptr = c_void_p() 65 | output = self._effect.dispatcher(byref(self._effect), c_int32(opcode), c_int32(index), 66 | vst_int_ptr(value), ptr, c_float(opt)) 67 | return output 68 | 69 | # Parameters 70 | # 71 | @property 72 | def num_params(self): 73 | return self._effect.num_params 74 | 75 | def _get_param_attr(self, index, opcode): 76 | # It should be VstStringConstants.kVstMaxParamStrLen == 8 but I've encountered some VST 77 | # with more that would segfault. 78 | buf = create_string_buffer(64) 79 | self._dispatch(opcode, index=index, ptr=byref(buf)) 80 | return string_at(buf).decode() 81 | 82 | def get_param_name(self, index): 83 | return self._get_param_attr(index, AEffectOpcodes.effGetParamName) 84 | 85 | def get_param_label(self, index): 86 | return self._get_param_attr(index, AEffectOpcodes.effGetParamLabel) 87 | 88 | def get_param_display(self, index): 89 | return self._get_param_attr(index, AEffectOpcodes.effGetParamDisplay) 90 | 91 | def get_param_value(self, index): 92 | return self._effect.get_parameter(byref(self._effect), c_int(index)) 93 | 94 | def set_param_value(self, index, value): 95 | self._effect.set_parameter(byref(self._effect), index, value) 96 | 97 | def get_param_properties(self, index): 98 | props = VstParameterProperties() 99 | self._dispatch(AEffectOpcodes.effGetParameterProperties, index=index, ptr=byref(props)) 100 | return props 101 | 102 | @property 103 | def vst_version(self): 104 | return self._dispatch(AEffectOpcodes.effGetVstVersion) 105 | 106 | @property 107 | def num_inputs(self): 108 | return self._effect.num_inputs 109 | 110 | @property 111 | def num_outputs(self): 112 | return self._effect.num_outputs 113 | 114 | @property 115 | def num_midi_in(self): 116 | return self._dispatch(AEffectOpcodes.effGetNumMidiInputChannels) 117 | 118 | @property 119 | def num_midi_out(self): 120 | return self._dispatch(AEffectOpcodes.effGetNumMidiOutputChannels) 121 | 122 | def get_input_properties(self, index): 123 | props = VstPinProperties() 124 | is_supported = self._dispatch(AEffectOpcodes.effGetInputProperties, index=index, 125 | ptr=byref(props)) 126 | props.is_supported = is_supported 127 | return props 128 | 129 | def get_output_properties(self, index): 130 | props = VstPinProperties() 131 | is_supported = self._dispatch(AEffectOpcodes.effGetOutputProperties, index=index, ptr=byref(props)) 132 | props.is_supported = is_supported 133 | return props 134 | 135 | @property 136 | def plug_category(self): 137 | return VstPlugCategory(self._dispatch(AEffectOpcodes.effGetPlugCategory)) 138 | 139 | # Processing 140 | # 141 | 142 | def _allocate_array(self, shape, c_type): 143 | assert len(shape) == 2 144 | insides = [(c_type * shape[1])() for i in range(shape[0])] 145 | out = (POINTER(c_type) * shape[0])(*insides) 146 | return out 147 | 148 | def process(self, input=None, sample_frames=None, double=None): 149 | 150 | if double is None: 151 | if input is not None: 152 | double = input.dtype == np.float64 153 | else: 154 | double = self.can_double_replacing 155 | 156 | if double: 157 | c_type = c_double 158 | process_fn = self._effect.process_double_replacing 159 | else: 160 | c_type = c_float 161 | process_fn = self._effect.process_replacing 162 | 163 | if input is None: 164 | input = self._allocate_array((self.num_inputs, sample_frames), c_type) 165 | else: 166 | input = (POINTER(c_type) * self.num_inputs)(*[row.ctypes.data_as(POINTER(c_type)) 167 | for row in input]) 168 | if sample_frames is None: 169 | raise ValueError('You must provide `sample_frames` when there is no input') 170 | 171 | output = self._allocate_array((self.num_outputs, sample_frames), c_type) 172 | 173 | process_fn( 174 | byref(self._effect), 175 | input, 176 | output, 177 | sample_frames, 178 | ) 179 | 180 | output = np.vstack([ 181 | np.ctypeslib.as_array(output[i], shape=(sample_frames,)) 182 | for i in range(self.num_outputs) 183 | ]) 184 | return output 185 | 186 | def process_events(self, vst_events): 187 | self._dispatch(AEffectOpcodes.effProcessEvents, ptr=byref(vst_events)) 188 | 189 | def set_block_size(self, max_block_size): 190 | self._dispatch(AEffectOpcodes.effSetBlockSize, value=max_block_size) 191 | 192 | def set_sample_rate(self, sample_rate): 193 | self._dispatch(AEffectOpcodes.effSetSampleRate, opt=sample_rate) 194 | 195 | @property 196 | def is_synth(self): 197 | return self._effect.flags & VstAEffectFlags.effFlagsIsSynth 198 | 199 | @property 200 | def can_double_replacing(self): 201 | return bool(self._effect.flags & VstAEffectFlags.effFlagsCanDoubleReplacing) 202 | -------------------------------------------------------------------------------- /pyvst/vstwrap.py: -------------------------------------------------------------------------------- 1 | from ctypes import (Structure, POINTER, CFUNCTYPE, c_void_p, c_float, 2 | c_int32, c_double, c_char, c_int16, c_int64) 3 | from enum import IntEnum 4 | 5 | 6 | # Corresponds to VstIntPtr in aeffect.h 7 | # We're assuming we are working in 64bit 8 | vst_int_ptr = c_int64 9 | 10 | 11 | class AudioMasterOpcodes(IntEnum): 12 | # [index]: parameter index [opt]: parameter value @see AudioEffect::setParameterAutomated 13 | audioMasterAutomate = 0 14 | # [return value]: Host VST version (for example 2400 for VST 2.4) @see AudioEffect::getMasterVersion 15 | audioMasterVersion = 1 16 | # [return value]: current unique identifier on shell plug-in @see AudioEffect::getCurrentUniqueId 17 | audioMasterCurrentId = 2 18 | # no arguments @see AudioEffect::masterIdle 19 | audioMasterIdle = 3 20 | 21 | # \deprecated deprecated in VST 2.4 r2 22 | # DECLARE_VST_DEPRECATED (audioMasterPinConnected) 23 | # \deprecated deprecated in VST 2.4 24 | audioMasterWantMidi = 6 25 | 26 | # [return value]: #VstTimeInfo* or null if not supported [value]: request mask @see VstTimeInfoFlags @see AudioEffectX::getTimeInfo 27 | audioMasterGetTime = 7 28 | # [ptr]: pointer to #VstEvents @see VstEvents @see AudioEffectX::sendVstEventsToHost 29 | audioMasterProcessEvents = 8 30 | 31 | # \deprecated deprecated in VST 2.4 32 | # DECLARE_VST_DEPRECATED (audioMasterSetTime), 33 | # \deprecated deprecated in VST 2.4 34 | # DECLARE_VST_DEPRECATED (audioMasterTempoAt), 35 | # \deprecated deprecated in VST 2.4 36 | # DECLARE_VST_DEPRECATED (audioMasterGetNumAutomatableParameters), 37 | # \deprecated deprecated in VST 2.4 38 | # DECLARE_VST_DEPRECATED (audioMasterGetParameterQuantization), 39 | 40 | # [return value]: 1 if supported @see AudioEffectX::ioChanged 41 | audioMasterIOChanged = 13 42 | 43 | # \deprecated deprecated in VST 2.4 44 | # DECLARE_VST_DEPRECATED (audioMasterNeedIdle), 45 | 46 | # [index]: new width [value]: new height [return value]: 1 if supported @see AudioEffectX::sizeWindow 47 | audioMasterSizeWindow = 15 48 | # [return value]: current sample rate @see AudioEffectX::updateSampleRate 49 | audioMasterGetSampleRate = 16 50 | # [return value]: current block size @see AudioEffectX::updateBlockSize 51 | audioMasterGetBlockSize = 17 52 | # [return value]: input latency in audio samples @see AudioEffectX::getInputLatency 53 | audioMasterGetInputLatency = 18 54 | # [return value]: output latency in audio samples @see AudioEffectX::getOutputLatency 55 | audioMasterGetOutputLatency = 19 56 | 57 | # \deprecated deprecated in VST 2.4 58 | # DECLARE_VST_DEPRECATED (audioMasterGetPreviousPlug), 59 | # \deprecated deprecated in VST 2.4 60 | # DECLARE_VST_DEPRECATED (audioMasterGetNextPlug), 61 | # \deprecated deprecated in VST 2.4 62 | # DECLARE_VST_DEPRECATED (audioMasterWillReplaceOrAccumulate), 63 | 64 | # [return value]: current process level @see VstProcessLevels 65 | audioMasterGetCurrentProcessLevel = 23 66 | # [return value]: current automation state @see VstAutomationStates 67 | audioMasterGetAutomationState = 24 68 | 69 | # [index]: numNewAudioFiles [value]: numAudioFiles [ptr]: #VstAudioFile* @see AudioEffectX::offlineStart 70 | audioMasterOfflineStart = 25 71 | # [index]: bool readSource [value]: #VstOfflineOption* @see VstOfflineOption [ptr]: #VstOfflineTask* @see VstOfflineTask @see AudioEffectX::offlineRead 72 | audioMasterOfflineRead = 26 73 | # @see audioMasterOfflineRead @see AudioEffectX::offlineRead 74 | audioMasterOfflineWrite = 27 75 | # @see AudioEffectX::offlineGetCurrentPass 76 | audioMasterOfflineGetCurrentPass = 28 77 | # @see AudioEffectX::offlineGetCurrentMetaPass 78 | audioMasterOfflineGetCurrentMetaPass = 29 79 | 80 | # \deprecated deprecated in VST 2.4 81 | # DECLARE_VST_DEPRECATED (audioMasterSetOutputSampleRate), 82 | # \deprecated deprecated in VST 2.4 83 | # DECLARE_VST_DEPRECATED (audioMasterGetOutputSpeakerArrangement), 84 | 85 | # [ptr]: char buffer for vendor string, limited to #kVstMaxVendorStrLen @see AudioEffectX::getHostVendorString 86 | audioMasterGetVendorString = 32 87 | # [ptr]: char buffer for vendor string, limited to #kVstMaxProductStrLen @see AudioEffectX::getHostProductString 88 | audioMasterGetProductString = 33 89 | # [return value]: vendor-specific version @see AudioEffectX::getHostVendorVersion 90 | audioMasterGetVendorVersion = 34 91 | # no definition, vendor specific handling @see AudioEffectX::hostVendorSpecific 92 | audioMasterVendorSpecific = 35 93 | 94 | # \deprecated deprecated in VST 2.4 95 | # DECLARE_VST_DEPRECATED (audioMasterSetIcon), 96 | 97 | # [ptr]: "can do" string [return value]: 1 for supported 98 | audioMasterCanDo= 37 99 | # [return value]: language code @see VstHostLanguage 100 | audioMasterGetLanguage = 38 101 | 102 | # \deprecated deprecated in VST 2.4 103 | # DECLARE_VST_DEPRECATED (audioMasterOpenWindow), 104 | # \deprecated deprecated in VST 2.4 105 | # DECLARE_VST_DEPRECATED (audioMasterCloseWindow), 106 | 107 | # [return value]: FSSpec on MAC, else char* @see AudioEffectX::getDirectory 108 | audioMasterGetDirectory = 41 109 | # no arguments 110 | audioMasterUpdateDisplay = 42 111 | # [index]: parameter index @see AudioEffectX::beginEdit 112 | audioMasterBeginEdit = 43 113 | # [index]: parameter index @see AudioEffectX::endEdit 114 | audioMasterEndEdit = 44 115 | # [ptr]: VstFileSelect* [return value]: 1 if supported @see AudioEffectX::openFileSelector 116 | audioMasterOpenFileSelector = 45 117 | # [ptr]: VstFileSelect* @see AudioEffectX::closeFileSelector 118 | audioMasterCloseFileSelector = 46 119 | 120 | # \deprecated deprecated in VST 2.4 121 | # DECLARE_VST_DEPRECATED (audioMasterEditFile), 122 | 123 | # \deprecated deprecated in VST 2.4 [ptr]: char[2048] or sizeof (FSSpec) [return value]: 1 if supported @see AudioEffectX::getChunkFile 124 | # DECLARE_VST_DEPRECATED (audioMasterGetChunkFile), 125 | 126 | # \deprecated deprecated in VST 2.4 127 | # DECLARE_VST_DEPRECATED (audioMasterGetInputSpeakerArrangement) 128 | 129 | 130 | class AEffectOpcodes(IntEnum): 131 | # no arguments @see AudioEffect::open 132 | effOpen = 0 133 | # no arguments @see AudioEffect::close 134 | effClose = 1 135 | 136 | # [value]: new program number @see AudioEffect::setProgram 137 | effSetProgram = 2 138 | # [return value]: current program number @see AudioEffect::getProgram 139 | effGetProgram = 3 140 | # [ptr]: char* with new program name, limited to #kVstMaxProgNameLen @see AudioEffect::setProgramName 141 | effSetProgramName = 4 142 | # [ptr]: char buffer for current program name, limited to #kVstMaxProgNameLen @see AudioEffect::getProgramName 143 | effGetProgramName = 5 144 | 145 | # [ptr]: char buffer for parameter label, limited to #kVstMaxParamStrLen @see AudioEffect::getParameterLabel 146 | effGetParamLabel = 6 147 | # [ptr]: char buffer for parameter display, limited to #kVstMaxParamStrLen @see AudioEffect::getParameterDisplay 148 | effGetParamDisplay = 7 149 | # [ptr]: char buffer for parameter name, limited to #kVstMaxParamStrLen @see AudioEffect::getParameterName 150 | effGetParamName = 8 151 | # \deprecated deprecated in VST 2.4 152 | # DECLARE_VST_DEPRECATED (effGetVu) 153 | 154 | # [opt]: new sample rate for audio processing @see AudioEffect::setSampleRate 155 | effSetSampleRate = 10 156 | # [value]: new maximum block size for audio processing @see AudioEffect::setBlockSize 157 | effSetBlockSize = 11 158 | # [value]: 0 means "turn off", 1 means "turn on" @see AudioEffect::suspend @see AudioEffect::resume 159 | effMainsChanged = 12 160 | 161 | # [ptr]: #ERect** receiving pointer to editor size @see ERect @see AEffEditor::getRect 162 | effEditGetRect = 13 163 | # [ptr]: system dependent Window pointer, e.g. HWND on Windows @see AEffEditor::open 164 | effEditOpen = 14 165 | # no arguments @see AEffEditor::close 166 | effEditClose = 15 167 | 168 | # \deprecated deprecated in VST 2.4 169 | # DECLARE_VST_DEPRECATED (effEditDraw) 170 | # deprecated deprecated in VST 2.4 171 | # DECLARE_VST_DEPRECATED (effEditMouse) 172 | # deprecated deprecated in VST 2.4 173 | # DECLARE_VST_DEPRECATED (effEditKey) 174 | 175 | # no arguments @see AEffEditor::idle 176 | effEditIdle = 19 177 | 178 | # deprecated deprecated in VST 2.4 179 | # DECLARE_VST_DEPRECATED (effEditTop) 180 | # deprecated deprecated in VST 2.4 181 | # DECLARE_VST_DEPRECATED (effEditSleep) 182 | # deprecated deprecated in VST 2.4 183 | # DECLARE_VST_DEPRECATED (effIdentify) 184 | 185 | # [ptr]: void** for chunk data address [index]: 0 for bank, 1 for program @see AudioEffect::getChunk 186 | effGetChunk = 23 187 | # [ptr]: chunk data [value]: byte size [index]: 0 for bank, 1 for program @see AudioEffect::setChunk 188 | effSetChunk = 24 189 | 190 | # [ptr]: #VstEvents* @see AudioEffectX::processEvents 191 | effProcessEvents = 25 192 | 193 | # [index]: parameter index [return value]: 1=true, 0=false @see AudioEffectX::canParameterBeAutomated 194 | effCanBeAutomated = 26 195 | # [index]: parameter index [ptr]: parameter string [return value]: true for success @see AudioEffectX::string2parameter 196 | effString2Parameter = 27 197 | 198 | # \deprecated deprecated in VST 2.4 199 | # DECLARE_VST_DEPRECATED (effGetNumProgramCategories) 200 | 201 | # [index]: program index [ptr]: buffer for program name, limited to #kVstMaxProgNameLen [return value]: true for success @see AudioEffectX::getProgramNameIndexed 202 | effGetProgramNameIndexed = 29 203 | 204 | # \deprecated deprecated in VST 2.4 205 | # DECLARE_VST_DEPRECATED (effCopyProgram) 206 | # \deprecated deprecated in VST 2.4 207 | # DECLARE_VST_DEPRECATED (effConnectInput) 208 | # \deprecated deprecated in VST 2.4 209 | # DECLARE_VST_DEPRECATED (effConnectOutput) 210 | 211 | # [index]: input index [ptr]: #VstPinProperties* [return value]: 1 if supported @see AudioEffectX::getInputProperties 212 | effGetInputProperties = 33 213 | # [index]: output index [ptr]: #VstPinProperties* [return value]: 1 if supported @see AudioEffectX::getOutputProperties 214 | effGetOutputProperties = 34 215 | # [return value]: category @see VstPlugCategory @see AudioEffectX::getPlugCategory 216 | effGetPlugCategory = 35 217 | 218 | # \deprecated deprecated in VST 2.4 219 | # DECLARE_VST_DEPRECATED (effGetCurrentPosition) 220 | # \deprecated deprecated in VST 2.4 221 | # DECLARE_VST_DEPRECATED (effGetDestinationBuffer) 222 | 223 | # [ptr]: #VstAudioFile array [value]: count [index]: start flag @see AudioEffectX::offlineNotify 224 | effOfflineNotify = 38 225 | # [ptr]: #VstOfflineTask array [value]: count @see AudioEffectX::offlinePrepare 226 | effOfflinePrepare = 39 227 | # [ptr]: #VstOfflineTask array [value]: count @see AudioEffectX::offlineRun 228 | effOfflineRun = 40 229 | 230 | # [ptr]: #VstVariableIo* @see AudioEffectX::processVariableIo 231 | effProcessVarIo = 41 232 | # [value]: input #VstSpeakerArrangement* [ptr]: output #VstSpeakerArrangement* @see AudioEffectX::setSpeakerArrangement 233 | effSetSpeakerArrangement = 42 234 | 235 | # \deprecated deprecated in VST 2.4 236 | # DECLARE_VST_DEPRECATED (effSetBlockSizeAndSampleRate) 237 | 238 | # [value]: 1 = bypass, 0 = no bypass @see AudioEffectX::setBypass 239 | effSetBypass = 44 240 | # [ptr]: buffer for effect name limited to #kVstMaxEffectNameLen @see AudioEffectX::getEffectName 241 | effGetEffectName = 45 242 | 243 | # \deprecated deprecated in VST 2.4 244 | # DECLARE_VST_DEPRECATED (effGetErrorText) 245 | 246 | # [ptr]: buffer for effect vendor string, limited to #kVstMaxVendorStrLen @see AudioEffectX::getVendorString 247 | effGetVendorString = 47 248 | # [ptr]: buffer for effect vendor string, limited to #kVstMaxProductStrLen @see AudioEffectX::getProductString 249 | effGetProductString = 48 250 | # [return value]: vendor-specific version @see AudioEffectX::getVendorVersion 251 | effGetVendorVersion = 49 252 | # no definition, vendor specific handling @see AudioEffectX::vendorSpecific 253 | effVendorSpecific = 50 254 | # [ptr]: "can do" string [return value]: 0: "don't know" -1: "no" 1: "yes" @see AudioEffectX::canDo 255 | effCanDo = 51 256 | # [return value]: tail size (for example the reverb time of a reverb plug-in); 0 is default (return 1 for 'no tail') 257 | effGetTailSize = 52 258 | 259 | # \deprecated deprecated in VST 2.4 260 | # DECLARE_VST_DEPRECATED (effIdle) 261 | # \deprecated deprecated in VST 2.4 262 | # DECLARE_VST_DEPRECATED (effGetIcon) 263 | # \deprecated deprecated in VST 2.4 264 | # DECLARE_VST_DEPRECATED (effSetViewPosition) 265 | 266 | # [index]: parameter index [ptr]: #VstParameterProperties* [return value]: 1 if supported @see AudioEffectX::getParameterProperties 267 | effGetParameterProperties = 56 268 | 269 | # \deprecated deprecated in VST 2.4 270 | # DECLARE_VST_DEPRECATED (effKeysRequired) 271 | 272 | # [return value]: VST version @see AudioEffectX::getVstVersion 273 | effGetVstVersion = 58 274 | 275 | # [value]: @see VstProcessPrecision @see AudioEffectX::setProcessPrecision 276 | effSetProcessPrecision = 59 277 | # [return value]: number of used MIDI input channels (1-15) @see AudioEffectX::getNumMidiInputChannels 278 | effGetNumMidiInputChannels = 60 279 | # [return value]: number of used MIDI output channels (1-15) @see AudioEffectX::getNumMidiOutputChannels 280 | effGetNumMidiOutputChannels = 61 281 | 282 | 283 | class VstStringConstants: 284 | # used for #effGetProgramName, #effSetProgramName, #effGetProgramNameIndexed 285 | kVstMaxProgNameLen = 24 286 | # used for #effGetParamLabel, #effGetParamDisplay, #effGetParamName 287 | kVstMaxParamStrLen = 8 288 | # used for #effGetVendorString, #audioMasterGetVendorString 289 | kVstMaxVendorStrLen = 64 290 | # used for #effGetProductString, #audioMasterGetProductString 291 | kVstMaxProductStrLen = 64 292 | # used for #effGetEffectName 293 | kVstMaxEffectNameLen = 32 294 | 295 | 296 | class Vst2StringConstants: 297 | # used for #MidiProgramName, #MidiProgramCategory, #MidiKeyName, #VstSpeakerProperties, #VstPinProperties 298 | kVstMaxNameLen = 64 299 | # used for #VstParameterProperties->label, #VstPinProperties->label 300 | kVstMaxLabelLen = 64 301 | # used for #VstParameterProperties->shortLabel, #VstPinProperties->shortLabel 302 | kVstMaxShortLabelLen = 8 303 | # used for #VstParameterProperties->label 304 | kVstMaxCategLabelLen = 24 305 | # used for #VstAudioFile->name 306 | kVstMaxFileNameLen = 100 307 | 308 | 309 | class AEffect(Structure): 310 | pass 311 | 312 | 313 | # typedef VstIntPtr (VSTCALLBACK *audioMasterCallback) (AEffect* effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void* ptr, float opt); 314 | AUDIO_MASTER_CALLBACK_TYPE = CFUNCTYPE(vst_int_ptr, POINTER(AEffect), c_int32, c_int32, vst_int_ptr, c_void_p, c_float) 315 | # typedef VstIntPtr (VSTCALLBACK *AEffectDispatcherProc) (AEffect* effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void* ptr, float opt); 316 | _AEFFECT_DISPATCHER_PROC_TYPE = CFUNCTYPE(vst_int_ptr, POINTER(AEffect), c_int32, c_int32, vst_int_ptr, c_void_p, c_float) 317 | # typedef void (VSTCALLBACK *AEffectProcessProc) (AEffect* effect, float** inputs, float** outputs, VstInt32 sampleFrames); 318 | # _AEFFECT_PROCESS_PROC_TYPE = CFUNCTYPE(c_void_p, 319 | # POINTER(AEffect), 320 | # POINTER(POINTER(c_float)), 321 | # POINTER(POINTER(c_float)), 322 | # c_int32,) 323 | _GET_PARAMETER_TYPE = CFUNCTYPE(c_float, POINTER(AEffect), c_int32) 324 | _SET_PARAMETER_TYPE = CFUNCTYPE(None, POINTER(AEffect), c_int32, c_float) 325 | # typedef void (VSTCALLBACK *AEffectProcessProc) (AEffect* effect, float** inputs, float** outputs, VstInt32 sampleFrames); 326 | _AEFFECT_PROCESS_PROC = CFUNCTYPE(None, POINTER(AEffect), POINTER(POINTER(c_float)), POINTER(POINTER(c_float)), c_int32) 327 | # typedef void (VSTCALLBACK *AEffectProcessDoubleProc) (AEffect* effect, double** inputs, double** outputs, VstInt32 sampleFrames); 328 | _AEFFECT_PROCESS_DOUBLE_PROC = CFUNCTYPE(None, POINTER(AEffect), POINTER(POINTER(c_double)), POINTER(POINTER(c_double)), c_int32) 329 | 330 | 331 | AEffect._fields_ = [ 332 | ('magic', c_int32), 333 | ('dispatcher', _AEFFECT_DISPATCHER_PROC_TYPE), 334 | ('_process', c_void_p), 335 | ('set_parameter', _SET_PARAMETER_TYPE), 336 | ('get_parameter', _GET_PARAMETER_TYPE), 337 | ('num_programs', c_int32), 338 | ('num_params', c_int32), 339 | ('num_inputs', c_int32), 340 | ('num_outputs', c_int32), 341 | ('flags', c_int32), 342 | ('resvd1', vst_int_ptr), 343 | ('resvd2', vst_int_ptr), 344 | ('initial_delay', c_int32), 345 | ('_realQualities', c_int32), 346 | ('_offQualities', c_int32), 347 | ('_ioRatio', c_float), 348 | ('object', c_void_p), 349 | ('user', c_void_p), 350 | ('unique_id', c_int32), 351 | ('version', c_int32), 352 | ('process_replacing', _AEFFECT_PROCESS_PROC), 353 | ('process_double_replacing', _AEFFECT_PROCESS_DOUBLE_PROC), 354 | ('_future1', c_char * 56), 355 | ] 356 | 357 | 358 | class VstParameterProperties(Structure): 359 | _fields_ = [ 360 | ('step_float', c_float), 361 | ('small_step_float', c_float), 362 | ('large_step_float', c_float), 363 | ('label', c_char * Vst2StringConstants.kVstMaxLabelLen), 364 | ('flags', c_int32), 365 | ('min_int', c_int32), 366 | ('max_int', c_int32), 367 | ('step_int', c_int32), 368 | ('large_step_int', c_int32), 369 | ('short_label', c_char * Vst2StringConstants.kVstMaxShortLabelLen), 370 | ('display_index', c_int16), 371 | ('category', c_int16), 372 | ('num_params_in_category', c_int16), 373 | ('reserved', c_int16), 374 | ('category_label', c_char * Vst2StringConstants.kVstMaxCategLabelLen), 375 | ('future', c_char * 16), 376 | ] 377 | 378 | 379 | class VstPinProperties(Structure): 380 | _fields_ = [ 381 | # pin name 382 | ('label', c_char * 64), # FIXME same 383 | # VstPinPropertiesFlags 384 | ('flags', c_int32), 385 | # VstSpeakerArrangementType 386 | ('arrangement_type', c_int32), 387 | # short name (recommende 6 + delimiter) 388 | ('short_label', c_char * 8), # FIXME same 389 | ('future', c_char * 48), 390 | ] 391 | 392 | 393 | # Note: In python3.6 we could use `IntFlag`. 394 | class VstParameterFlags(IntEnum): 395 | # parameter is a switch (on/off) 396 | kVstParameterIsSwitch = 1 << 0 397 | # minInteger, maxInteger valid 398 | kVstParameterUsesIntegerMinMax = 1 << 1 399 | # stepFloat, smallStepFloat, largeStepFloat valid 400 | kVstParameterUsesFloatStep = 1 << 2 401 | # stepInteger, largeStepInteger valid 402 | kVstParameterUsesIntStep = 1 << 3 403 | # displayIndex valid 404 | kVstParameterSupportsDisplayIndex = 1 << 4 405 | # category, etc. valid 406 | kVstParameterSupportsDisplayCategory = 1 << 5 407 | # set if parameter value can ramp up/down 408 | kVstParameterCanRamp = 1 << 6 409 | 410 | 411 | class VstAEffectFlags(IntEnum): 412 | # set if the plug-in provides a custom editor 413 | effFlagsHasEditor = 1 << 0 414 | # supports replacing process mode (which should the default mode in VST 2.4) 415 | effFlagsCanReplacing = 1 << 4 416 | # program data is handled in formatless chunks 417 | effFlagsProgramChunks = 1 << 5 418 | # plug-in is a synth (VSTi), Host may assign mixer channels for its outputs 419 | effFlagsIsSynth = 1 << 8 420 | # plug-in does not produce sound when input is all silence 421 | effFlagsNoSoundInStop = 1 << 9 422 | 423 | # plug-in supports double precision processing 424 | effFlagsCanDoubleReplacing = 1 << 12 425 | 426 | # \deprecated deprecated in VST 2.4 427 | # DECLARE_VST_DEPRECATED (effFlagsHasClip) = 1 << 1, 428 | # \deprecated deprecated in VST 2.4 429 | # DECLARE_VST_DEPRECATED (effFlagsHasVu) = 1 << 2, 430 | # \deprecated deprecated in VST 2.4 431 | # DECLARE_VST_DEPRECATED (effFlagsCanMono) = 1 << 3, 432 | # \deprecated deprecated in VST 2.4 433 | # DECLARE_VST_DEPRECATED (effFlagsExtIsAsync) = 1 << 10, 434 | # \deprecated deprecated in VST 2.4 435 | # DECLARE_VST_DEPRECATED (effFlagsExtHasBuffer) = 1 << 11 436 | 437 | 438 | class VstEvent(Structure): 439 | _fields_ = [ 440 | # @see VstEventTypes 441 | ('type', c_int32), 442 | # size of this event, excl. type and byteSize 443 | ('byte_size', c_int32), 444 | # sample frames related to the current block start sample position 445 | ('delta_frames', c_int32), 446 | # generic flags, none defined yet 447 | ('flags', c_int32), 448 | # data size may vary, depending on event type 449 | ('data', c_char * 16), 450 | ] 451 | 452 | 453 | class VstEventTypes(IntEnum): 454 | # MIDI event @see VstMidiEvent 455 | kVstMidiType = 1 456 | # \deprecated unused event type 457 | # DECLARE_VST_DEPRECATED (kVstAudioType), 458 | # \deprecated unused event type 459 | # DECLARE_VST_DEPRECATED (kVstVideoType), 460 | # \deprecated unused event type 461 | # DECLARE_VST_DEPRECATED (kVstParameterType), 462 | # \deprecated unused event type 463 | # DECLARE_VST_DEPRECATED (kVstTriggerType), 464 | # MIDI system exclusive @see VstMidiSysexEvent 465 | kVstSysExType = 6 466 | 467 | 468 | class VstEvents(Structure): 469 | _fields_ = [ 470 | # number of Events in array 471 | ('num_events', c_int32), 472 | # zero (Reserved for future use) 473 | ('reserved', vst_int_ptr), 474 | # event pointer array, variable size 475 | ('events', POINTER(VstEvent) * 2), 476 | ] 477 | 478 | def get_vst_events_struct(num_events): 479 | """Class factory to get a VstEvents class with the right length for the "events" field.""" 480 | class VstEventsN(Structure): 481 | _fields_ = [ 482 | # number of Events in array 483 | ('num_events', c_int32), 484 | # zero (Reserved for future use) 485 | ('reserved', vst_int_ptr), 486 | # event pointer array, variable size 487 | ('events', POINTER(VstEvent) * num_events), 488 | ] 489 | return VstEventsN 490 | 491 | 492 | class VstMidiEvent(Structure): 493 | _fields_ = [ 494 | # kVstMidiType 495 | ('type', c_int32), 496 | # sizeof (VstMidiEvent) 497 | ('byte_size', c_int32), 498 | # sample frames related to the current block start sample position 499 | ('delta_frames', c_int32), 500 | # @see VstMidiEventFlags 501 | ('flags', c_int32), 502 | # (in sample frames) of entire note, if available, else 0 503 | ('note_length', c_int32), 504 | # offset (in sample frames) into note from note start if available, else 0 505 | ('note_offset', c_int32), 506 | # 1 to 3 MIDI bytes; midiData[3] is reserved (zero) 507 | ('midi_data', c_char * 4), 508 | # -64 to +63 cents; for scales other than 'well-tempered' ('microtuning') 509 | ('detune', c_char), 510 | # Note Off Velocity [0, 127] 511 | ('note_off_velocity', c_char), 512 | # zero (Reserved for future use) 513 | ('reserved1', c_char), 514 | # zero (Reserved for future use) 515 | ('reserved2', c_char), 516 | ] 517 | 518 | 519 | # Note: In python3.6 we could use `IntFlag`. 520 | class VstMidiEventFlags(IntEnum): 521 | # means that this event is played life (not in playback from a sequencer track). 522 | # This allows the Plug-In to handle these flagged events with higher priority, especially when 523 | # the Plug-In has a big latency (AEffect::initialDelay) 524 | kVstMidiEventIsRealtime = 1 << 0 525 | 526 | 527 | class VstTimeInfo(Structure): 528 | __fields__ = [ 529 | # current Position in audio samples (always valid) 530 | ('sample_pos', c_double), 531 | # current Sample Rate in Herz (always valid) 532 | ('sample_rate', c_double), 533 | # System Time in nanoseconds (10^-9 second) 534 | ('nano_seconds', c_double), 535 | # Musical Position, in Quarter Note (1.0 equals 1 Quarter Note) 536 | ('ppq_pos', c_double), 537 | # current Tempo in BPM (Beats Per Minute) 538 | ('tempo', c_double), 539 | # last Bar Start Position, in Quarter Note 540 | ('bar_start_pos', c_double), 541 | # Cycle Start (left locator), in Quarter Note 542 | ('cycle_start_pos', c_double), 543 | # Cycle End (right locator), in Quarter Note 544 | ('cycle_end_pos', c_double), 545 | # Time Signature Numerator (e.g. 3 for 3/4) 546 | ('time_sig_numerator', c_int32), 547 | # Time Signature Denominator (e.g. 4 for 3/4) 548 | ('time_sig_denominator', c_int32), 549 | # SMPTE offset (in SMPTE subframes (bits; 1/80 of a frame)). The current SMPTE position can be calculated using #samplePos, #sampleRate, and #smpteFrameRate. 550 | ('smpte_offset', c_int32), 551 | # @see VstSmpteFrameRate 552 | ('smpte_frame_rate', c_int32), 553 | # MIDI Clock Resolution (24 Per Quarter Note), can be negative (nearest clock) 554 | ('samples_to_next_clock', c_int32), 555 | # @see VstTimeInfoFlags 556 | ('flags', c_int32), 557 | ] 558 | 559 | class VstTimeInfoFlags(IntEnum): 560 | # indicates that play, cycle or record state has changed 561 | kVstTransportChanged = 1 562 | # set if Host sequencer is currently playing 563 | kVstTransportPlaying = 1 << 1 564 | # set if Host sequencer is in cycle mode 565 | kVstTransportCycleActive = 1 << 2 566 | # set if Host sequencer is in record mode 567 | kVstTransportRecording = 1 << 3 568 | # set if automation write mode active (record parameter changes) 569 | kVstAutomationWriting = 1 << 6 570 | # set if automation read mode active (play parameter changes) 571 | kVstAutomationReading = 1 << 7 572 | # VstTimeInfo::nanoSeconds valid 573 | kVstNanosValid = 1 << 8 574 | # VstTimeInfo::ppqPos valid 575 | kVstPpqPosValid = 1 << 9 576 | # VstTimeInfo::tempo valid 577 | kVstTempoValid = 1 << 10 578 | # VstTimeInfo::barStartPos valid 579 | kVstBarsValid = 1 << 11 580 | # VstTimeInfo::cycleStartPos and VstTimeInfo::cycleEndPos valid 581 | kVstCyclePosValid = 1 << 12 582 | # VstTimeInfo::timeSigNumerator and VstTimeInfo::timeSigDenominator valid 583 | kVstTimeSigValid = 1 << 13 584 | # VstTimeInfo::smpteOffset and VstTimeInfo::smpteFrameRate valid 585 | kVstSmpteValid = 1 << 14 586 | # VstTimeInfo::samplesToNextClock valid 587 | kVstClockValid = 1 << 15 588 | 589 | 590 | class VstAudioFileFlags(IntEnum): 591 | # set by Host (in call #offlineNotify) 592 | kVstOfflineReadOnly = 1 << 0 593 | # set by Host (in call #offlineNotify) 594 | kVstOfflineNoRateConversion = 1 << 1 595 | # set by Host (in call #offlineNotify) 596 | kVstOfflineNoChannelChange = 1 << 2 597 | # set by plug-in (in call #offlineStart) 598 | kVstOfflineCanProcessSelection = 1 << 10 599 | # set by plug-in (in call #offlineStart) 600 | kVstOfflineNoCrossfade = 1 << 11 601 | # set by plug-in (in call #offlineStart) 602 | kVstOfflineWantRead = 1 << 12 603 | # set by plug-in (in call #offlineStart) 604 | kVstOfflineWantWrite = 1 << 13 605 | # set by plug-in (in call #offlineStart) 606 | kVstOfflineWantWriteMarker = 1 << 14 607 | # set by plug-in (in call #offlineStart) 608 | kVstOfflineWantMoveCursor = 1 << 15 609 | # set by plug-in (in call #offlineStart) 610 | kVstOfflineWantSelect = 1 << 16 611 | 612 | 613 | class VstPlugCategory(IntEnum): 614 | # Unknown, category not implemented 615 | kPlugCategUnknown = 0 616 | # Simple Effect 617 | kPlugCategEffect = 1 618 | # VST Instrument (Synths, samplers,...) 619 | kPlugCategSynth = 2 620 | # Scope, Tuner, ... 621 | kPlugCategAnalysis = 3 622 | # Dynamics, ... 623 | kPlugCategMastering = 4 624 | # Panners, ... 625 | kPlugCategSpacializer = 5 626 | # Delays and Reverbs 627 | kPlugCategRoomFx = 6 628 | # Dedicated surround processor 629 | kPlugSurroundFx = 7 630 | # Denoiser, ... 631 | kPlugCategRestoration = 8 632 | # Offline Process 633 | kPlugCategOfflineProcess = 9 634 | # Plug-in is container of other plug-ins @see effShellGetNextPlugin 635 | kPlugCategShell = 10 636 | # ToneGenerator, ... 637 | kPlugCategGenerator = 11 638 | # Marker to count the categories 639 | kPlugCategMaxCount = 12 640 | -------------------------------------------------------------------------------- /scripts/print_plugin.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from pyvst import VstPlugin 4 | 5 | 6 | def _main(filename): 7 | plugin = VstPlugin(filename) 8 | 9 | # TODO print more than that! 10 | print('-- Parameters --') 11 | for index in range(plugin.num_params): 12 | print('[{}] {} = {} {}'.format( 13 | index, 14 | plugin.get_param_name(index), 15 | plugin.get_param_value(index), 16 | plugin.get_param_label(index) 17 | )) 18 | 19 | 20 | if __name__ == '__main__': 21 | parser = argparse.ArgumentParser() 22 | parser.add_argument('vst', help='path to .so file') 23 | args = parser.parse_args() 24 | 25 | _main(args.vst) 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | setup( 5 | name='pyvst', 6 | version='0.5.0', 7 | description='VST2.4 python wrapping using ctypes', 8 | author='Simon Lemieux', 9 | author_email='lemieux.simon (at) gmail (dot) (you know what)', 10 | url='https://github.com/simlmx/pyvst', 11 | packages = find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), 12 | install_requires=[ 13 | 'numpy>=1.16.0', 14 | ], 15 | extras_require={ 16 | 'dev': [ 17 | 'pytest>=3.8.0', 18 | ], 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /tests/test_example.py: -------------------------------------------------------------------------------- 1 | from examples.simple_host import main 2 | 3 | 4 | def test_simple_host_example(vst): 5 | main(vst) 6 | -------------------------------------------------------------------------------- /tests/test_host.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import pytest 4 | 5 | from pyvst import SimpleHost 6 | from pyvst.host import Transport 7 | 8 | 9 | def test_transport(): 10 | transport = Transport(sample_rate=48000., tempo=120.) 11 | block_size = 512 12 | 13 | transport.step(block_size) 14 | assert transport.has_changed 15 | assert transport.is_playing 16 | 17 | transport.step(block_size) 18 | assert not transport.has_changed 19 | assert transport.is_playing 20 | assert transport.get_position() == block_size * 2 21 | 22 | transport.stop() 23 | assert transport.has_changed 24 | assert not transport.is_playing 25 | assert transport.get_position() == block_size * 2 26 | 27 | transport.reset() 28 | assert not transport.has_changed 29 | assert not transport.is_playing 30 | assert transport.get_position() == 0 31 | 32 | 33 | def test_transport_get_position_units(): 34 | sample_rate = 48000. 35 | tempo = 120. 36 | beat_per_sec = tempo / 60. 37 | block_size = 512 38 | transport = Transport(sample_rate=sample_rate, tempo=tempo) 39 | 40 | transport.step(block_size) 41 | transport.step(block_size) 42 | 43 | assert transport.get_position() == block_size * 2 44 | assert transport.get_position('frame') == block_size * 2 45 | assert transport.get_position('second') == block_size * 2 / sample_rate 46 | assert transport.get_position('beat') == beat_per_sec * (block_size * 2 / sample_rate) 47 | 48 | 49 | def test_play_note(host): 50 | 51 | # small max_duration compared to midi duration 52 | with pytest.raises(ValueError, match='is smaller than the midi note_duration'): 53 | host.play_note(64, note_duration=2., max_duration=1.) 54 | 55 | # smaller max_duration than min_duration 56 | with pytest.raises(ValueError, match='is smaller than min_duration'): 57 | host.play_note(64, note_duration=1., max_duration=1., min_duration=2.) 58 | 59 | # Try to play a note with a given duration 60 | output = host.play_note(note=76, velocity=127, note_duration=.2, max_duration=3., 61 | min_duration=3.) 62 | assert output.shape == (2, 44100 * 3) 63 | # Make sure there was some noise! 64 | assert output.max() > .1 65 | 66 | # Automatic stopping of the sound 67 | output = host.play_note(64, note_duration=0.1, max_duration=60.) 68 | assert 44100 * 0.1 < output.shape[1] < 44100 * 58 69 | 70 | 71 | def test_play_note_twice(host): 72 | vel = 127 73 | sound1 = host.play_note(note=64, min_duration=1., max_duration=2., note_duration=1., 74 | velocity=vel) 75 | sound2 = host.play_note(note=65, min_duration=1., max_duration=2., note_duration=1., 76 | velocity=vel) 77 | # import numpy as np 78 | # np.save('patate1.npy', sound1) 79 | # np.save('patate2.npy', sound2) 80 | # TODO compare with something more resistant to noise 81 | # assert abs(sound1 - sound2).mean() / abs(sound1).mean() < 0.001 82 | 83 | # after changing all the parameters, it should still work 84 | for i in range(host.vst.num_params): 85 | host.vst.set_param_value(i, random.random()) 86 | 87 | # TODO same 88 | sound1 = host.play_note() 89 | sound2 = host.play_note() 90 | 91 | 92 | # FIXME: For the same reason as above, this is unreliable 93 | # def test_play_note_changing_params(host): 94 | # sound1 = host.play_note() 95 | 96 | # for i in range(host.vst.num_params): 97 | # host.vst.set_param_value(i, random.random()) 98 | 99 | # sound2 = host.play_note() 100 | 101 | # for i in range(host.vst.num_params): 102 | # host.vst.set_param_value(i, random.random()) 103 | 104 | # sound3 = host.play_note() 105 | 106 | # assert sound1.shape != sound2.shape or abs(sound1 - sound2).mean() / abs(sound1).mean() > 0.0001 107 | # assert sound2.shape != sound3.shape or abs(sound2 - sound3).mean() / abs(sound2).mean() > 0.0001 108 | -------------------------------------------------------------------------------- /tests/test_midi.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | from pyvst.midi import midi_note_as_bytes, midi_note_event, wrap_vst_events, all_sounds_off_event 3 | from pyvst.vstwrap import VstMidiEvent 4 | 5 | 6 | def test_note_on_bytes(): 7 | assert midi_note_as_bytes(10, 10, 'note_on', 2) == b'\x91\x0A\x0A' 8 | assert midi_note_as_bytes(100, 100, 'note_off', 16) == b'\x8F\x64\x64' 9 | 10 | 11 | def test_midi_note_event(): 12 | midi_note_event(64, 100) 13 | # TODO test something 14 | 15 | 16 | def test_all_sounds_off_event(): 17 | # The last 0 disappears... I think it might be normal 18 | assert bytes(all_sounds_off_event().midi_data) == b'\xb0\x78' 19 | 20 | 21 | def test_wrap_vst_events(): 22 | notes = [midi_note_event(64 + i, 100) for i in range(3)] 23 | wrapped = wrap_vst_events(notes) 24 | assert wrapped.num_events == 3 25 | events = wrapped.events 26 | note1 = events[0].contents 27 | assert note1.byte_size == ctypes.sizeof(VstMidiEvent) 28 | assert events[2].contents.byte_size == ctypes.sizeof(VstMidiEvent) 29 | 30 | note3 = events[2] 31 | note3 = ctypes.cast(note3, ctypes.POINTER(VstMidiEvent)).contents 32 | -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | # import numpy as np 2 | 3 | from pyvst import VstPlugin, SimpleHost 4 | 5 | 6 | def test_plugin(vst): 7 | vst = VstPlugin(vst) 8 | assert vst.num_params > 0 9 | 10 | # All the vsts we test are synths 11 | assert vst.is_synth 12 | 13 | 14 | def test_get_set_param(vst): 15 | vst = VstPlugin(vst) 16 | vst.set_param_value(0, 1.) 17 | assert vst.get_param_value(0) == 1. 18 | vst.set_param_value(0, .2) 19 | assert (vst.get_param_value(0) - .2) / 2. < 0.00001 20 | 21 | 22 | def test_open_close(vst): 23 | vst = VstPlugin(vst) 24 | vst.open() 25 | vst.close() 26 | 27 | 28 | def test_segfault(vst): 29 | """ 30 | Reproducing a weird segfault. 31 | It segfaults with numpy>=1.14 ... no idea why. 32 | """ 33 | host = SimpleHost() 34 | 35 | vst = VstPlugin(vst, host._callback) 36 | vst.set_sample_rate(44100.) 37 | vst.set_block_size(512) 38 | 39 | import numpy as np 40 | print(np.ones(shape=(1, 1))) 41 | vst.process(sample_frames=512) 42 | --------------------------------------------------------------------------------