Skip to content

Commit

Permalink
Capture the stdout/stderr of the plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
simlmx committed Nov 20, 2018
1 parent 6dc9d7b commit 79d33bd
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 26 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Next Release

* Capture the stdout/stderr of the plugin.
* Change the default play_note max_duration to 5 seconds.

# 0.3.0

* Big fixes in SimpleHost + general cleaning. Also now `SimpleHost.play_note` as defaults for all
Expand Down
7 changes: 4 additions & 3 deletions pyvst/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@ def vst(self):
raise RuntimeError('You must first load a vst using `self.load_vst`.')
return self._vst

def load_vst(self, path_to_so_file=None):
def load_vst(self, path_to_so_file=None, verbose=False):
"""
Loads a vst. If there was already a vst loaded, we will release it.
:param path_to_so_file: Path to the .so file to use as a plugin. If we call this without
any path, we will simply try to reload using the same path as the last call.
:param verbose: Set to False (default) to capture the VST's stdout/stderr.
"""
reloading = False
if path_to_so_file is None:
Expand All @@ -94,7 +95,7 @@ def load_vst(self, path_to_so_file=None):
params = [self._vst.get_param_value(i) for i in range(self._vst.num_params)]
del self._vst

self._vst = VstPlugin(path_to_so_file, self._callback)
self._vst = VstPlugin(path_to_so_file, self._callback, verbose=verbose)

# If we are reloading the same VST, put back the parameters where they were.
if reloading:
Expand All @@ -112,7 +113,7 @@ def load_vst(self, path_to_so_file=None):
# We note the path so that we can easily reload it!
self._vst_path = path_to_so_file

def play_note(self, note=64, note_duration=.5, velocity=100, max_duration=60.,
def play_note(self, note=64, note_duration=.5, velocity=100, max_duration=5.,
min_duration=0.01, volume_threshold=0.000002):
"""
:param note_duration: Duration between the note on and note off midi events, in seconds.
Expand Down
24 changes: 22 additions & 2 deletions pyvst/tests/test_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,32 @@ def test_play_note(host):
def test_play_note_twice(host):
sound1 = host.play_note()
sound2 = host.play_note()
assert abs(sound1 - sound2).mean() < 0.001
assert abs(sound1 - sound2).mean() / abs(sound1).mean() < 0.001

# after changing all the parameters, it should still work
for i in range(host.vst.num_params):
host.vst.set_param_value(i, random.random())

sound1 = host.play_note()
sound2 = host.play_note()
assert abs(sound1 - sound2).mean() < 0.001
# FIXME: This actually often sound the same but doesn't have the exact same numbers. Needs
# revisiting.
# assert abs(sound1 - sound2).mean() / abs(sound1).mean() < 0.0001


# FIXME: For the same reason as above, this is unreliable
# def test_play_note_changing_params(host):
# sound1 = host.play_note()

# for i in range(host.vst.num_params):
# host.vst.set_param_value(i, random.random())

# sound2 = host.play_note()

# for i in range(host.vst.num_params):
# host.vst.set_param_value(i, random.random())

# sound3 = host.play_note()

# assert sound1.shape != sound2.shape or abs(sound1 - sound2).mean() / abs(sound1).mean() > 0.0001
# assert sound2.shape != sound3.shape or abs(sound2 - sound3).mean() / abs(sound2).mean() > 0.0001
51 changes: 31 additions & 20 deletions pyvst/vstplugin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import contextlib
from ctypes import (cdll, Structure, POINTER, CFUNCTYPE,
c_void_p, c_int, c_float, c_int32, c_double, c_char,
addressof, byref, pointer, cast, string_at, create_string_buffer)
from warnings import warn

import numpy
from wurlitzer import pipes

from .vstwrap import (
AudioMasterOpcodes,
Expand All @@ -30,13 +32,22 @@ def _default_audio_master_callback(effect, opcode, *args):


class VstPlugin:
def __init__(self, filename, audio_master_callback=None):
def __init__(self, filename, audio_master_callback=None, verbose=False):
"""
:param verbose: Set to True to show the plugin's stdout/stderr. By default (False),
we capture it.
"""
self.verbose = verbose

if audio_master_callback is None:
audio_master_callback = _default_audio_master_callback
self._lib = cdll.LoadLibrary(filename)
self._lib.VSTPluginMain.argtypes = [AUDIO_MASTER_CALLBACK_TYPE]
self._lib.VSTPluginMain.restype = POINTER(AEffect)
self._effect = self._lib.VSTPluginMain(AUDIO_MASTER_CALLBACK_TYPE(audio_master_callback)).contents

# Capture stdout and stderr, unless in verbose mode
with pipes() if not verbose else contextlib.suppress():
self._effect = self._lib.VSTPluginMain(AUDIO_MASTER_CALLBACK_TYPE(audio_master_callback)).contents

assert self._effect.magic == MAGIC

Expand All @@ -59,14 +70,15 @@ def _dispatch(self, opcode, index=0, value=0, ptr=None, opt=0.):
if ptr is None:
ptr = c_void_p(None)
# self._effect.dispatcher.argtypes = [POINTER(AEffect), c_int32, c_int32, c_int, c_void_p, c_float]
output = self._effect.dispatcher(byref(self._effect), c_int32(opcode), c_int32(index), c_int(value), ptr, c_float(opt))
with pipes() if not self.verbose else contextlib.suppress():
output = self._effect.dispatcher(byref(self._effect), c_int32(opcode), c_int32(index), c_int(value), ptr, c_float(opt))
return output

# Parameters
#
@property
def num_params(self):
return self._effect.numParams
return self._effect.num_params

def _get_param_attr(self, index, opcode):
# It should be VstStringConstants.kVstMaxParamStrLen == 8 but I've encountered some VST
Expand Down Expand Up @@ -99,6 +111,14 @@ def get_param_properties(self, index):
def vst_version(self):
return self._dispatch(AEffectOpcodes.effGetVstVersion)

@property
def num_inputs(self):
return self._effect.num_inputs

@property
def num_outputs(self):
return self._effect.num_outputs

@property
def num_midi_in(self):
return self._dispatch(AEffectOpcodes.effGetNumMidiInputChannels)
Expand Down Expand Up @@ -146,12 +166,13 @@ def process(self, input=None, sample_frames=None):

output = self._make_empty_array(sample_frames, self.num_outputs)

self._effect.process_replacing(
byref(self._effect),
input,
output,
sample_frames
)
with pipes() if not self.verbose else contextlib.suppress():
self._effect.process_replacing(
byref(self._effect),
input,
output,
sample_frames
)

output = numpy.vstack([numpy.ctypeslib.as_array(output[i], shape=(sample_frames,))
for i in range(self.num_outputs)])
Expand All @@ -165,13 +186,3 @@ def set_block_size(self, max_block_size):

def set_sample_rate(self, sample_rate):
self._dispatch(AEffectOpcodes.effSetSampleRate, opt=sample_rate)

#
# TODO explicitely implement those, it makes it less confusing
def __getattr__(self, attr):
"""We also try getattr(self._effect, attr) so we don't have to wrap all of those."""
try:
return getattr(self._effect, attr)
except AttributeError:
pass
raise AttributeError('object VstPlugin has no attribute "{0}" and effect has no attribute "{0}'.format(attr))
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
url='https://github.com/simlmx/pyvst',
packages = find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
install_requires=[
'numpy>=1.15.1'
'numpy>=1.15.1',
'wurlitzer==1.0.1',
],
extras_require={
'dev': [
Expand Down

0 comments on commit 79d33bd

Please sign in to comment.