Ableton wird nicht benötigt: Verbinden Sie Ableton Push 2 mit dem VCV-Rack


Das Erstellen von Musik verläuft in letzter Zeit ähnlich wie ein Foto vor 10 Jahren: Jedes hat seinen eigenen DSLR- und Instagram-Account. Die Musikindustrie freut sich sehr darüber, denn ein solches Interesse bringt viel Geld. Jeden Tag erscheinen neue VST-Plugins und analoge Geräte, die Anzahl der Themenkurse wächst rasant, Blogs, die sich der Musikproduktion widmen, sind auf dem Top-Youtube zu finden. Vor diesem Hintergrund sehen Open-Source-Projekte sehr interessant aus und ermöglichen es jedem, der sich als Produzent versuchen möchte, ohne viel Geld dafür auszugeben. Ein solches Projekt ist VCV Rack, mit dem die teuerste Klasse von Musikinstrumenten - analoge modulare Synthesizer - für jedermann zugänglich gemacht werden soll. Als ich kürzlich auf die Idee für einen Track kam,und zur Hand gab es nur einen Linux-Laptop, ich wollte den ganzen Track in VCV machen. Unterwegs bestand der Wunsch, die Synthesizer mithilfe des MIDI-Controllers so zu steuern, dass die Verwendung der verfügbaren Module nicht funktioniert. Am Ende habe ich beschlossen, ein Plugin zu schreiben, um Ableton Push 2 mit VCV Rack zu verbinden. In diesem Artikel werden wir darüber sprechen, was daraus wurde und wie wir unsere eigenen Module für VCV schreiben.

Kurzübersicht
VCV Rack — open source , . VCV , .


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



VCV-Rack-API


Jedes Modul in VCV besteht aus zwei Teilen - Audio und Grafik. Der Audioteil erbt von der Modulklasse die Prozessmethode , die für jedes Sample aufgerufen wird, dh mit einer Sampling-Frequenz. Übrigens kann die Abtastfrequenz in VCV von Standard 44,1 kHz bis zu 768 kHz variieren, was eine genauere Emulation modularer Synthesizer bei ausreichender Rechenleistung ermöglicht.

Objekte des ModuleWidget Typs sind verantwortlich für die Grafik , die die erben Ziehverfahren . VCV verwendet die Nanovg-Vektorgrafikbibliothek. Zeichnung im Inneren des ZiehverfahrenDies kann sowohl innerhalb der Modulgrenzen (der Überschuss wird von der Engine abgeschnitten) als auch beispielsweise im Framebuffer auftreten, den wir noch verwenden.

Was braucht es also, um Ihr Modul für VCV zu schreiben?

Einrichten der Umgebung und Installieren des Rack SDK


Der erste Schritt ist in der Dokumentation gut beschrieben und verursacht keine Schwierigkeiten (zumindest unter Linux), daher werden wir nicht weiter darauf eingehen.

Generieren Sie eine Plugin-Vorlage


Dafür gibt es im Rack SDK ein helper.py-Skript. Er muss createplugin sagen und dann den Namen des Plugins und optional Informationen darüber angeben.

<Rack SDK folder>/helper.py createplugin MyPlugin

Wenn die Plugin-Vorlage erstellt wird, kann sie mit dem Befehl kompiliert und installiert werden

RACK_DIR=<Rack SDK folder> make install

Zeichnen Sie das Frontend des Moduls


Jedes Plugin kann mehrere Module enthalten, und für jedes der Module müssen Sie ein Hauptfenster zeichnen. Zu diesem Zweck bietet uns die VCV-Dokumentation die Verwendung von Inkscape oder einem anderen Vektoreditor. Da die Module in VCV auf einem virtuellen Eurorack-Ständer montiert sind, beträgt ihre Höhe immer 128,5 mm und die Breite sollte ein Vielfaches von 5,08 mm betragen.
Grundlegende Schnittstellenelemente wie CV / Gate-Sockel, Tasten und Glühbirnen können in Vektorform markiert werden. Zeichnen Sie dazu an ihrer Stelle Kreise der entsprechenden Farben ( weitere Details hier ), damit helper.py dann Code für dieses Markup generiert . Persönlich schien es mir, dass dies keine sehr praktische Funktion ist und es einfacher ist, Elemente direkt aus dem Code zu platzieren. Wenn das Bild und das Layout fertig sind, müssen Sie helper.py erneut ausführen um eine Modulvorlage zu erstellen und sie der Frontplatte zuzuordnen.

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

Wir verbinden Knöpfe und Drehungen




Abgesehen von der Anzeige wird Ableton Push 2 vom Computer als normales USB-MIDI-Gerät angesehen, wodurch es einfach ist, eine Verbindung zwischen ihm und dem VCV-Rack herzustellen. Erstellen Sie dazu eine Eingabe-MIDI-Warteschlange und einen Ausgabe-MIDI-Port innerhalb der Klasse des Moduls.

Initialisierungscode
struct AbletonPush2 : Module {
    midi::Output midiOutput;
    midi::InputQueue midiInput;

    bool inputConnected;
    bool outputConnected;
}


Versuchen wir, Ableton Push unter den angeschlossenen MIDI-Geräten zu finden und eine Verbindung herzustellen. Da das Modul ausschließlich für die Verwendung mit Ableton Push ausgelegt ist, müssen wir den Benutzer nicht mit einer Auswahl an Geräten belasten, sondern Sie können es einfach anhand des Namens finden.

Controller-Verbindungscode
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;
        }
    }
}


Jetzt können Sie in der Prozessmethode regelmäßig überprüfen, ob der Controller angeschlossen ist, und die Midi-Warteschlange fragen, ob Nachrichten eingegangen sind. Hier ist zu erwähnen, was und wie im Midi-Standard generell codiert ist. Tatsächlich gibt es zwei Haupttypen von Nachrichten: Note ON / OFF, die die Notennummer und die Druckkraft überträgt, und CC - Command Control, die den numerischen Wert einiger variabler Parameter überträgt. Weitere Informationen zu Midi finden Sie hier .

MIDI Queue Polling Code
void process(const ProcessArgs &args) override {
    if (sampleCounter > args.sampleRate / updateFrequency) {

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

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


Jetzt möchte ich den Controller-Pads beibringen, in verschiedenen Farben zu leuchten, damit sie bequemer navigiert werden können. Dazu müssen wir ihm den entsprechenden Midi-Befehl senden. Betrachten Sie zum Beispiel Pad Nummer 36 (dies ist das Pad unten links). Wenn Sie darauf klicken, sendet der Controller den Befehl 0x90 (Note on), gefolgt von der Note Nummer (36) und einer Nummer von 0 bis 127, was die Stärke der Presse bedeutet. Wenn der Computer im Gegenteil den gleichen Befehl 0x90 und die gleiche Notennummer 36 an den Controller sendet, gibt die dritte Nummer die Farbe an, mit der das untere linke Pad leuchten soll. Die Nummern der Tasten und Pads sind in der obigen Abbildung dargestellt. Push bietet zahlreiche Möglichkeiten zum Arbeiten mit Farben, Paletten, Animationen und anderen Hintergrundbeleuchtungsparametern. Ich ging nicht auf Details ein und brachte einfach alle möglichen Farben der Standardpalette auf die Pads und wählte die aus, die mir gefielen.Im allgemeinen Fall sieht die Konvertierung von Midi-Befehlen in PWM-Werte auf LEDs laut Dokumentation jedoch folgendermaßen aus:



Steuercode für die Hintergrundbeleuchtung
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);
}


Wir verbinden das Display




Um das Display anzuschließen, müssen Sie mit USB arbeiten. Der Controller selbst kann nichts zeichnen und weiß nichts über Grafiken. Er möchte lediglich mindestens alle 2 Sekunden einen Frame mit einer Größe von 160 x 960 Pixel senden. Das gesamte Rendern erfolgt auf der Seite des Computers. Schließen Sie das USB-Gerät an und öffnen Sie es, wie in der Dokumentation beschrieben :

Verbindungscode anzeigen
#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);
}


Um einen Frame zu übertragen, müssen Sie zuerst einen 16-Byte-Header und dann 160 Zeilen mit jeweils 960 16-Bit-Nummern senden. Gleichzeitig sollten Zeichenfolgen laut Dokumentation nicht in 1920 Bytes übertragen werden, sondern in Paketen von 2048 Bytes, ergänzt durch Nullen.

Frame Transfer Code
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);
    }
}


Jetzt muss nur noch ein Frame gezeichnet und in den Puffer geschrieben werden. Verwenden Sie dazu die FramebufferWidget- Klasse , die die drawFramebuffer- Methode implementiert . Das sofort einsatzbereite VCV-Rack verwendet die Nanovg- Bibliothek , daher ist es ziemlich einfach, hier Grafiken zu schreiben. Wir lernen den aktuellen grafischen Kontext aus der globalen Variablen APP und speichern ihren Status. Als nächstes wird eine leere 160x960 Rahmen erstellen und mit den DRAW - Ziehverfahren . Kopieren Sie anschließend den Framebuffer in das Array, das über USB gesendet wird, und geben Sie den Status des Kontexts zurück. Setzen Sie am Ende das Dirty- Flag, damit die VCV-Engine bei der nächsten Iteration des Renderings nicht vergisst, unser Widget zu aktualisieren.

Frame-Rendering-Code
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;
}


Logik der Interaktion und Zuordnung von Parametern


Für meine Aufgabe wollte ich in der Lage sein, in Sequenzern aufgezeichnete Muster zu wechseln und gleichzeitig einen bestimmten Satz von Parametern zu steuern, meinen eigenen für jede Gruppe von Mustern. Unter Mapping verstehe ich die Bindung des Parameters eines Moduls an den Parameter eines anderen Moduls oder MIDI-Controllers. Ich habe nur sehr wenige Informationen darüber gefunden, wie man Mapping schön macht, deshalb habe ich den größten Teil des Codes, der es implementiert, aus dem in VCV integrierten MIDI Map- Modul übernommen . Kurz gesagt, für jede Reihe von Parametern wird dort ein spezielles ParamHandle- Objekt erstellt , das der Engine über Krücken mitteilt, was mit was verbunden ist.

Fazit


Hier ist ein Modul, das ich am Ende bekommen habe. Zusätzlich zur Standardkonvertierung von Midi in CV können Sie Pads gruppieren, ihnen Farben zuweisen und Push-Encodern beliebige Parameter von Modulen zuordnen, deren Namen und Werte auf dem Controller-Display angezeigt werden. Im Video unten sehen Sie seine Übersicht und in diesem Video können Sie in Aktion sehen.


Den vollständigen Code finden Sie hier .

Wir haben es nur unter Linux (Ubuntu 18.04) getestet, unter MacOS konnte das Display aufgrund der Besonderheiten von libusb keine Verbindung herstellen und es gab merkwürdige Verzögerungen in der MIDI-Oberfläche. Trotzdem hat VCV Rack sowohl als Framework als auch als modulare DAW einen sehr guten Eindruck hinterlassen. Ich hoffe, dass sich VCV weiterentwickelt und dieser Artikel jemand anderem hilft, seine eigenen Module dafür zu schreiben.

All Articles