No se necesita Ableton: conecte Ableton Push 2 al bastidor VCV


La creación de música, últimamente, va de la misma manera que una fotografía hace 10 años: cada uno tiene su propia DSLR y cuenta de Instagram. La industria de la música está muy contenta con esto, porque ese interés genera mucho dinero. Todos los días, aparecen nuevos complementos VST y dispositivos analógicos, el número de cursos temáticos está creciendo rápidamente, los blogs dedicados a la producción de música están en la parte superior de YouTube. En este contexto, los proyectos de código abierto se ven muy interesantes, permitiendo a cualquiera que quiera probarse como productor sin gastar una tonelada de dinero en esto. Uno de estos proyectos es VCV Rack, que tiene como objetivo hacer que la clase más cara de instrumentos musicales, sintetizadores modulares analógicos, sea accesible para todos. Recientemente, cuando tuve la idea de una pista,y a la mano solo había una computadora portátil Linux, quería hacer toda la pista en VCV. Y en el camino, hubo un deseo de controlar los sintetizadores usando el controlador midi de una manera que el uso de los módulos disponibles no funcionó. Al final, decidí escribir un complemento para conectar Ableton Push 2 a VCV Rack. Hablaremos sobre lo que surgió y cómo escribir nuestros propios módulos para VCV en este artículo.

Referencia rápida
VCV Rack — open source , . VCV , .


Ableton Push 2 — midi- Ableton, DAW, API .



VCV Rack API


Cada módulo en VCV consta de dos partes: audio y gráfico. La parte de audio hereda de la clase Módulo el método de proceso , llamado para cada muestra, es decir, con una frecuencia de muestreo. Por cierto, la frecuencia de muestreo en VCV puede variar desde 44.1 kHz estándar hasta 768 kHz, lo que permite emular con mayor precisión los sintetizadores modulares en presencia de suficiente potencia informática.

Los objetos del tipo ModuleWidget son responsables de los gráficos , que heredan el método de dibujo de la estructura base . VCV utiliza la biblioteca de gráficos vectoriales nanovg. Dibujando dentro del método de dibujopuede ocurrir tanto dentro de los límites del módulo (el exceso es cortado por el motor) como, por ejemplo, en el framebuffer, que todavía usamos.

Entonces, ¿qué se necesita para escribir su módulo para VCV?

Configuración del entorno e instalación del Rack SDK


El primer paso está bien descrito en la documentación y no causa ninguna dificultad (al menos en Linux), por lo que no nos detendremos en él.

Generar una plantilla de complemento


Hay un script helper.py para esto en el SDK de Rack. Necesita decir createplugin y luego especificar el nombre del complemento y, opcionalmente, información sobre él.

<Rack SDK folder>/helper.py createplugin MyPlugin

Cuando se crea la plantilla de complemento, se puede compilar e instalar con el comando

RACK_DIR=<Rack SDK folder> make install

Dibuja la interfaz del módulo


Cada complemento puede contener varios módulos, y para cada uno de los módulos debe dibujar un panel principal. Para esto, la documentación de VCV nos ofrece usar Inkscape o cualquier otro editor de vectores. Dado que los módulos en VCV están montados en un soporte virtual Eurorack, su altura siempre es de 128.5 mm y el ancho debe ser un múltiplo de 5.08 mm.
Los elementos básicos de la interfaz, como los enchufes CV / Gate, los botones y las bombillas se pueden marcar en forma vectorial. Para hacer esto, dibuje en su lugar círculos de los colores correspondientes ( más detalles aquí ), de modo que helper.py genere código para este marcado. Personalmente, me pareció que esta no es una característica muy conveniente y es más fácil colocar elementos directamente desde el código. Cuando la imagen y el diseño estén listos, debe ejecutar helper.py nuevamente para crear una plantilla de módulo y asociarla con el panel frontal.

helper.py createmodule MyModule res/MyModule.svg src/MyModule.cpp

Conectamos botones y giros




Además de la pantalla, Ableton Push 2 es visto por la computadora como un dispositivo USB-MIDI normal, lo que hace que sea fácil establecer una conexión entre él y VCV Rack. Para hacer esto, cree una cola midi de entrada y un puerto midi de salida dentro de la clase del módulo.

Código de inicialización
struct AbletonPush2 : Module {
    midi::Output midiOutput;
    midi::InputQueue midiInput;

    bool inputConnected;
    bool outputConnected;
}


Intentemos encontrar Ableton Push entre los dispositivos midi conectados y conectarnos a él. Dado que el módulo está diseñado para funcionar exclusivamente con Ableton Push, no necesitamos cargar al usuario con una elección de dispositivo y simplemente puede encontrarlo por su nombre.

Código de conexión del controlador
void connectPush() {
    auto in_devs = midiInput.getDeviceIds();
    for (int i = 0; i < in_devs.size(); i++){
        if (midiInput.getDeviceName(in_devs[i]).find("Ableton") != std::string::npos) {
            midiInput.setDeviceId(in_devs[i]);
            inputConnected = true;
            break;
        }
    }
		
    auto out_devs = midiOutput.getDeviceIds();
    for (int i = 0; i < out_devs.size(); i++){
        if (midiOutput.getDeviceName(out_devs[i]).find("Ableton") != std::string::npos) {
            midiOutput.setDeviceId(out_devs[i]);
            outputConnected = true;
            break;
        }
    }
}


Ahora, en el método de proceso , puede verificar periódicamente si el controlador está conectado y preguntar a la cola midi si ha llegado algún mensaje. Aquí vale la pena mencionar qué y cómo se codifica generalmente en el estándar midi. De hecho, hay dos tipos principales de mensajes, estos son Nota ON / OFF, transmitiendo el número de nota y presionando fuerza, y CC - Control de comando, transmitiendo el valor numérico de algún parámetro variable. Más información sobre midi se puede encontrar aquí .

Código de sondeo de cola MIDI
void process(const ProcessArgs &args) override {
    if (sampleCounter > args.sampleRate / updateFrequency) {

        if ((!inputConnected) && (!outputConnected)) {
            connectPush();
        }

	midi::Message msg;
	while (midiInput.shift(&msg)) {
	    processMidi(msg);
	}
    }
}


Ahora quiero enseñarle a los pads del controlador a brillar en diferentes colores, para que sea más conveniente navegarlos. Para hacer esto, necesitaremos enviarle el comando midi apropiado. Considere, por ejemplo, el pad número 36 (este es el pad izquierdo más bajo). Si hace clic en él, el controlador enviará el comando 0x90 (nota activada), seguido del número de nota (36) y un número del 0 al 127, lo que significa la fuerza de la prensa. Y cuando, por el contrario, la computadora envía el mismo comando 0x90 y el mismo número de nota 36 al controlador, entonces el tercer número indicará el color con el que debería brillar la almohadilla inferior izquierda. Los números de los botones y las teclas se muestran en la figura anterior. Push tiene bastantes posibilidades para trabajar con color, paletas, animación y otros parámetros de retroiluminación. No entré en detalles y simplemente traje todos los colores posibles de la paleta predeterminada a los pads y elegí los que me gustaron.Pero en el caso general, la conversión de comandos midi a valores PWM en LED, de acuerdo con la documentación, se ve así:



Código de control de retroiluminación
void lightOn(int note, int color){
    midi::Message msg;
    msg.setNote(note);
    msg.setValue(color);
    msg.setChannel(1);
    msg.setStatus(0x9);
    midiOutput.sendMessage(msg);
}

void lightOff(int note){
    midi::Message msg;
    msg.setNote(note);
    msg.setValue(0);
    msg.setChannel(1);
    msg.setStatus(0x8);
    midiOutput.sendMessage(msg);
}


Conectamos la pantalla




Para conectar la pantalla, debe trabajar con USB. El controlador en sí no sabe cómo dibujar nada y no sabe nada sobre gráficos. Todo lo que quiere es enviar un marco de 160x960 píxeles de tamaño al menos cada 2 segundos. Todo el renderizado se realiza al costado de la computadora. Para comenzar, conecte y abra el dispositivo USB, como se describe en la documentación :

Mostrar código de conexión
#ifdef _MSC_VER
#define _CRT_SECURE_NO_WARNINGS
#endif

#include <stdio.h>

#ifdef _WIN32

// see following link for a discussion of the
// warning suppression:
// http://sourceforge.net/mailarchive/forum.php?
// thread_name=50F6011C.2020000%40akeo.ie&forum_name=libusbx-devel

// Disable: warning C4200: nonstandard extension used:
// zero-sized array in struct/union
#pragma warning(disable:4200)

#include <windows.h>
#endif

#ifdef __linux__
#include <libusb-1.0/libusb.h>
#else
#include "libusb.h"
#endif

#define ABLETON_VENDOR_ID 0x2982
#define PUSH2_PRODUCT_ID  0x1967

static libusb_device_handle* open_push2_device()
{
  int result;

  if ((result = libusb_init(NULL)) < 0)
  {
    printf("error: [%d] could not initilialize usblib\n", result);
    return NULL;
  }

  libusb_set_debug(NULL, LIBUSB_LOG_LEVEL_ERROR);

  libusb_device** devices;
  ssize_t count;
  count = libusb_get_device_list(NULL, &devices);
  if (count < 0)
  {
    printf("error: [%ld] could not get usb device list\n", count);
    return NULL;
  }

  libusb_device* device;
  libusb_device_handle* device_handle = NULL;

  char ErrorMsg[128];

  // set message in case we get to the end of the list w/o finding a device
  sprintf(ErrorMsg, "error: Ableton Push 2 device not found\n");

  for (int i = 0; (device = devices[i]) != NULL; i++)
  {
    struct libusb_device_descriptor descriptor;
    if ((result = libusb_get_device_descriptor(device, &descriptor)) < 0)
    {
      sprintf(ErrorMsg,
        "error: [%d] could not get usb device descriptor\n", result);
      continue;
    }

    if (descriptor.bDeviceClass == LIBUSB_CLASS_PER_INTERFACE
      && descriptor.idVendor == ABLETON_VENDOR_ID
      && descriptor.idProduct == PUSH2_PRODUCT_ID)
    {
      if ((result = libusb_open(device, &device_handle)) < 0)
      {
        sprintf(ErrorMsg,
          "error: [%d] could not open Ableton Push 2 device\n", result);
      }
      else if ((result = libusb_claim_interface(device_handle, 0)) < 0)
      {
        sprintf(ErrorMsg,
          "error: [%d] could not claim interface 0 of Push 2 device\n", result);
        libusb_close(device_handle);
        device_handle = NULL;
      }
      else
      {
        break; // successfully opened
      }
    }
  }

  if (device_handle == NULL)
  {
    printf(ErrorMsg);
  }

  libusb_free_device_list(devices, 1);
  return device_handle;
}

static void close_push2_device(libusb_device_handle* device_handle)
{
  libusb_release_interface(device_handle, 0);
  libusb_close(device_handle);
}


Para transmitir una trama, primero debe enviar un encabezado de 16 bytes y luego 160 líneas de 960 números de 16 bits cada una. Al mismo tiempo, según la documentación, las cadenas no deben transmitirse en 1920 bytes, sino en paquetes de 2048 bytes, complementados con ceros.

Código de transferencia de trama
struct Push2Display : FramebufferWidget {

    unsigned char frame_header[16] = {
          0xFF, 0xCC, 0xAA, 0x88,
          0x00, 0x00, 0x00, 0x00,
          0x00, 0x00, 0x00, 0x00,
          0x00, 0x00, 0x00, 0x00 
    };

    libusb_device_handle* device_handle;

    unsigned char* image;

    void sendDisplay(unsigned char * image) {
        int actual_length;
     
        libusb_bulk_transfer(device_handle, PUSH2_BULK_EP_OUT, frame_header, sizeof(frame_header), &actual_length, TRANSFER_TIMEOUT);
        for (int i = 0; i < 160; i++)
            libusb_bulk_transfer(device_handle, PUSH2_BULK_EP_OUT, &image[(159 - i)*1920], 2048, &actual_length, TRANSFER_TIMEOUT);
    }
}


Ahora solo queda dibujar y escribir un marco en el búfer. Para hacer esto, use la clase FramebufferWidget que implementa el método drawFramebuffer . VCV Rack listo para usar utiliza la biblioteca nanovg , por lo que es bastante fácil escribir gráficos aquí. Aprendemos el contexto gráfico actual de la variable global APP y guardamos su estado. A continuación, cree un marco vacío de 160x960 y dibuje con el método de dibujo . Después de eso, copie el framebuffer a la matriz que se enviará a través de USB y devuelva el estado del contexto. Al final, configure la bandera sucia para que en la próxima iteración del renderizado, el motor VCV no olvide actualizar nuestro widget.

Código de renderizado de cuadros
void drawFramebuffer() override {

    NVGcontext* vg = APP->window->vg;

    if (display_connected) {
        nvgSave(vg);
	glViewport(0, 0, 960, 160);
        glClearColor(0, 0, 0, 0);
        glClear(GL_COLOR_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);

	nvgBeginFrame(vg, 960,  160, 1);
        draw(vg);
        nvgEndFrame(vg);

        glReadPixels(0, 0, 960, 160, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, image); 

        //  XOR   ,  
        for (int i = 0; i < 80*960; i ++){
            image[i * 4] ^= 0xE7;
	    image[i * 4 + 1] ^= 0xF3;
            image[i * 4 + 2] ^= 0xE7;
	    image[i * 4 + 3] ^= 0xFF;
        }
	    	
	sendDisplay(image);

	nvgRestore(vg);
    }

    dirty = true;
}


Lógica de interacción y mapeo de parámetros.


Para mi tarea, quería cambiar los patrones grabados en secuenciadores y, al mismo tiempo, controlar un cierto conjunto de parámetros, el mío para cada grupo de patrones. Al mapear, entiendo la vinculación del parámetro de un módulo con el parámetro de otro módulo o controlador midi. Encontré muy poca información sobre cómo hacer un mapeo hermoso, así que tomé la mayor parte del código que lo implementa del módulo Mapa MIDI integrado en VCV . En resumen, para cada grupo de parámetros se crea un objeto ParamHandle especial allí , que le dice al motor a través de muletas qué está conectado con qué.

Conclusión


Aquí hay un módulo que obtuve al final. Además de la conversión estándar de midi a CV, le permite agrupar pads, asignarles colores y asociar parámetros de módulos arbitrarios con codificadores Push, mostrando sus nombres y valores en la pantalla del controlador. En el video a continuación puede ver su descripción general, y en este video puede ver en acción.


El código completo está disponible aquí .

Logramos probarlo solo bajo Linux (Ubuntu 18.04), en MacOS la pantalla no se conectó debido a los detalles de libusb y hubo retrasos extraños en la interfaz midi. A pesar de esto, VCV Rack dejó una muy buena impresión tanto como marco como como DAW modular, espero que VCV continúe desarrollándose, y este artículo ayudará a alguien más a escribir sus propios módulos para él.

All Articles