PyVST – Procesamiento de audio con VST

Versión: 2.x.
Descargas: pyvst, ATKUniversalDelay (plugin VST de ejemplo), utility.py (funciones de conversión), NumPy, ejemplos.zip.
Plataforma: Microsoft Windows.

Introducción

VST es una interfaz de procesamiento de audio y MIDI desarrollada por Steinberg pero adoptada por la mayoría del software del tipo DAW (Digital Audio Workstation), por ejemplo, Cubase, SONAR, Pro Tools. Otras interfaces similares incluyen RTAS (desarrollada por AVID) y Audio Units (Apple).

PyVST es un módulo escrito íntegramente en Python utilizando ctypes que permite interactuar con plugins VST (escritos en C/C++ y distribuidos como archivos DLL).

Un plugin VST está constituido, principalmente, por tres partes.

  • Un algoritmo de procesamiento de audio y/o MIDI.
  • Un conjunto de parámetros que permiten modificar diferentes aspectos del algoritmo. Estos pueden ser ajustados utilizando PyVST o bien…
  • Una interfaz gráfica.

El procesamiento de audio se realiza a través de las funciones process_replacing y process_double_replacing, de acuerdo a la capacidad de manejo de datos de 32 o 64 bits del plugin (que puede ser determinado utilizando can_process_double()). La función process llama a alguna de las dos funciones anteriores dependiendo del tipo de datos de entrada que se le han pasado como argumeto. Las tres funciones requieren como parámetro dos vectores de NumPy: uno de entrada y otro de salida.

Descarga e instalación

Una vez descargado el archivo ZIP y antes de iniciar la instalación es necesario realizar un pequeño cambio en el archivo pyvst/aeffect.py para prevenir errores durante la posterior ejecución. Simplemente reemplazar el siguiente bloque de código.

class VstStringConstants(object):
  kVstMaxProgNameLen = 24
  kVstMaxParamStrLen = 8
  kVstMaxVendorStrLen = 64
  kVstMaxProductStrLen = 64
  kVstMaxEffectNameLen = 32

Por este otro:

class VstStringConstants(object):
  kVstMaxProgNameLen = 256
  kVstMaxParamStrLen = 256
  kVstMaxVendorStrLen = 256
  kVstMaxProductStrLen = 256
  kVstMaxEffectNameLen = 256

Luego de guardar el archivo procedemos con la instalación.

python setup.py install

Para todos los ejemplos en este artículo puedes utilizar el plugin ATKUniversalDelay.

Cargar un plugin

Se importa la clase VSTPlugin y se pasa como argumento el nombre del archivo DLL.

from pyvst.vstplugin import VSTPlugin

plugin = VSTPlugin("ATKUniversalDelay.dll")

Puede obtenerse información del plugin utilizando la función dump_effect_properties.

from pyvst.vstplugin import dump_effect_properties, VSTPlugin

plugin = VSTPlugin("ATKUniversalDelay.dll")
dump_effect_properties(plugin)

Imprime en pantalla:

Plugin name: ATKUniversalDelay
Vendor name: MatthieuBrucher
Product name:
numPrograms = 1
numParams = 4
numInputs = 1
numOutputs = 1

Param 000: Delay [1.0 ms] (normalized = 0.301511)
Param 001: Blend [100.00 %] (normalized = 1.000000)
Param 002: Feedforward [50.00 %] (normalized = 0.750000)
Param 003: Feedback [0.00 %] (normalized = 0.500000)

Interfaz Gráfica

La mayoría de los plugins VST integran una interfaz gráfica. La función VSTPlugin.has_editor permite determinar si se encuentra disponible. La función VSTPlugin.open_edit espera como argumento el identificador (handle) de una ventana para incluir el editor.

A continuación tres ejemplos utilizando las librerías gráficas Tkinter, wxPython y PyQt.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# gui_tk.py

from pyvst.vstplugin import VSTPlugin
from Tkinter import Tk, Frame

plugin = VSTPlugin("ATKUniversalDelay.dll")

if plugin.has_editor():
    rect = plugin.get_erect()
    root = Tk()
    frame = Frame(root, height=rect.bottom, width=rect.right)
    frame.pack()
    root.title("Ejemplo PyVST con Tkinter")
    plugin.open_edit(frame.winfo_id())
    root.mainloop()
    plugin.close_edit()
else:
    print "El plugin no tiene editor."

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# gui_wx.py

from pyvst.vstplugin import VSTPlugin
import wx

plugin = VSTPlugin("ATKUniversalDelay.dll")

if plugin.has_editor():
    app = wx.App()
    frame = wx.Frame(None, -1, "Ejemplo PyVST con wxPython")
    plugin.open_edit(frame.GetHandle())
    rect = plugin.get_erect()
    frame.SetClientSize((rect.right, rect.bottom))
    frame.Show()
    app.MainLoop()
    plugin.close_edit()
else:
    print "El plugin no tiene editor."

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# gui_qt.py

from pyvst.vstplugin import VSTPlugin
from PyQt4.QtGui import QApplication, QMainWindow

plugin = VSTPlugin("ATKUniversalDelay.dll")

if plugin.has_editor():
    app = QApplication([])
    window = QMainWindow()
    window.setWindowTitle("Ejemplo PyVST con Qt")
    rect = plugin.get_erect()
    window.resize(rect.right, rect.bottom)
    window.show()
    plugin.open_edit(int(window.winId()))
    app.exec_()
    plugin.close_edit()
else:
    print "El plugin no tiene editor."

Vista previa

Parámetros

Cada parámetro de un plugin (en caso de tener) se encuentra identificado por un número. De esta forma, podemos obtener su valor utilizando VSTPlugin.get_parameter(numero_de_parametro) y análogamente ajustarlo con la función VSTPlugin.set_parameter(numero_de_parametro, nuevo_valor).

El siguiente código de ejemplo abre el editor del plugin y luego guarda los valores de los parámetros en un archivo de configuración.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from ConfigParser import SafeConfigParser

from pyvst.vstplugin import VSTPlugin
import wx


def main():
    plugin = VSTPlugin("ATKUniversalDelay.dll")
    
    app = wx.App()
    frame = wx.Frame(None, -1, "VST Interface")
    
    plugin.open_edit(frame.GetHandle())
    rect = plugin.get_erect()
    frame.SetClientSize((rect.right, rect.bottom))
    frame.Show()
    app.MainLoop()
    
    config = SafeConfigParser()
    config.add_section("Parameters")
    
    for i in range(plugin.get_number_of_parameters()):
        config.set("Parameters", "P" + str(i),
                   str(plugin.get_parameter(i)))
    
    with open("ATKUniversalDelay.ini", "w") as f:
        config.write(f)
    
    plugin.close()


if __name__ == "__main__":
    main()

Para cargar los valores almacenados debe utilizarse:

config = SafeConfigParser()
config.read("ATKUniversalDelay.ini")

for i in range(plugin.get_number_of_parameters()):
    plugin.set_parameter(
        i, config.getfloat("Parameters", "P" + str(i))
    )

Ejemplos

Ejemplo 1

Toma como entrada un archivo mono WAV (input.wav), lo procesa utilizando el plugin especificado y crea un nuevo archivo de nombre output.wav. Requiere SciPy.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pyvst.vstplugin import VSTPlugin
from scipy.io import wavfile
import numpy as np
from Tkinter import Tk, Frame

from utility import *


def main():
    # Cargar plugin VST.
    plugin = VSTPlugin("ATKUniversalDelay.dll")
    
    if plugin.has_editor():
        # Obtener dimensiones de la interfaz gráfica.
        rect = plugin.get_erect()
        
        # Crear ventana de Tkinter.
        root = Tk()
        frame = Frame(root, height=rect.bottom, width=rect.right)
        frame.pack()
        root.title("Ejemplo PyVST")
        plugin.open_edit(frame.winfo_id())
        root.mainloop()
    
    # Cargar el archivo WAV en un vector de NumPy y 
    # crear otro vector vacío de las mismas dimensiones
    # para almacenar el audio procesado por el plugin.
    rate, input_audio = wavfile.read("input.wav")
    # Convertir los valores a coma flotante de 32 / 64 bits
    # para poder ser pasados a la función process().
    float_type = "float" + str(32 * plugin.can_process_double() + 32)
    input_audio = pcm2float(input_audio, float_type)
    output_audio = np.zeros_like(input_audio)
    
    plugin.set_sample_rate(rate)
    plugin.set_block_size(2048)
    
    print "Processing ({})...".format(float_type)
    
    # Procesar el audio en bloques de 2048 bytes.
    for i in range(0, input_audio.size, 2048):
        plugin.process([input_audio[:i]], [output_audio[:i]])
    
    print "Done."
    
    # Cerrar el plugin y escribir el resultado en un archivo WAV.
    plugin.close()
    # Convertir nuevamente a entero de 16 bits.
    wavfile.write("output.wav", rate, float2pcm(output_audio, "int16"))


if __name__ == "__main__":
    main()

Ejemplo 2

Procesamiento de audio en tiempo real. Aplica el efecto del plugin VST a la entrada capturada por el micrófono y lo reproduce simultáneamente. Requiere PyAudio.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from ConfigParser import SafeConfigParser

from pyvst.vstplugin import VSTPlugin
import numpy as np
import pyaudio

from utility import *


def main():
    CHUNK = 1024
    FORMAT = pyaudio.paInt16
    CHANNELS = 1
    RATE = 44100
    
    print "Loading VST..."
    
    # Cargar plugin VST.
    plugin = VSTPlugin("ATKUniversalDelay.dll")
    # Determinar el tipo de datos soportado por el plugin.
    float_type = "float" + str(32 * plugin.can_process_double() + 32)
    
    # Cargar parámetros.
    config = SafeConfigParser()
    config.read("ATKUniversalDelay.ini")
    
    for i in range(plugin.get_number_of_parameters()):
        plugin.set_parameter(
            i, config.getfloat("Parameters", "P" + str(i))
        )
    
    plugin.set_sample_rate(RATE)
    plugin.set_block_size(CHUNK)
    
    print "Done."
    print "Opening stream..."
    
    # Abrir un canal de entrada y salida.
    p = pyaudio.PyAudio()
    stream = p.open(format=FORMAT,
                    channels=CHANNELS,
                    rate=RATE,
                    input=True,
                    output=True,
                    frames_per_buffer=CHUNK)

    print("Done.")

    while True:
        try:
            # Leer audio del micrófono.
            data = stream.read(CHUNK)
            # Convertir los valores a coma flotante.
            input_audio = pcm2float(np.fromstring(data, dtype="int16"),
                                    float_type)
            # Vector de almacenamiento para el audio procesado.
            output_audio = np.zeros_like(input_audio)
            # Procesar audio.
            plugin.process([input_audio], [output_audio])
            # Convertir nuevamente a entero de 16 bits.
            data = float2pcm(output_audio, "int16").tostring()
            # Reproducir.
            stream.write(data)
        except KeyboardInterrupt:
            break
        except Exception as e:
            from traceback import format_exc
            print format_exc()
            break

    print("Done recording.")

    stream.stop_stream()
    stream.close()
    plugin.close()
    p.terminate()


if __name__ == "__main__":
    main()

Curso online 👨‍💻

¡Ya lanzamos el curso oficial de Recursos Python en Udemy! Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.

Consultoría 💡

Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.

7 comentarios.

  1. No me deja importar la librería.
    Si introduzco:

    from pyvst.vstplugin import VSTPlugin

    Me aparece:

    Traceback (most recent call last):
    File «», line 1, in
    from pyvst.vstplugin import VSTPlugin
    File «C:\Users\thejo\AppData\Local\Programs\Python\Python37-32\lib\site-packages\pyvst-0.1.0-py3.7.egg\pyvst\__init__.py», line 2, in
    from vstplugin import VSTPlugin, dump_effect_properties
    ModuleNotFoundError: No module named ‘vstplugin’

  2. Hola, muchas gracias por contestar.
    Me acabo de registrar y acabo de publicar el tema en el foro…
    Muy buena la página, por cierto.
    Un saludo

  3. Hola,

    es genial poder encontrar este tutorial en castellano. Lo he probado y funciona perfectamente con los ejemplos que indicas.
    El problema que encuentro es que el plugin de muestra funciona con ‘float64’, pero al tratar de cargar otro plugin que trabaje en ‘float32’ me da errores, y no encuentro la solución.

    ¿sabrías como hacer que funcione?

    Muchísimas gracias,
    un cordial saludo.

    • Recursos Python says:

      Hola Ruben, me alegro que te haya servido. ¿Qué plugin VST estás utilizando y qué error obtuviste? Si te resulta más cómodo, te invito a registrarte y crear un tema en el foro con todos los detalles.

      Un saludo.

Deja una respuesta