Ableton n'est pas nécessaire: connectez Ableton Push 2 au rack VCV


La création de musique, ces derniers temps, ressemble beaucoup à une photographie il y a 10 ans: chacun a son propre compte DSLR et instagram. L'industrie de la musique en est très heureuse, car un tel intérêt rapporte beaucoup d'argent. Chaque jour, de nouveaux plugins VST et appareils analogiques apparaissent, le nombre de cours thématiques augmente rapidement, les blogs dédiés à la production musicale remontent sur le top youtube. Dans ce contexte, les projets open source semblent très intéressants, permettant à quiconque veut s'essayer en tant que producteur sans dépenser une tonne d'argent à ce sujet. L'un de ces projets est VCV Rack, qui vise à rendre la classe d'instruments de musique la plus chère - les synthétiseurs modulaires analogiques - accessible à tous. Récemment, quand j'ai eu l'idée d'un morceau,et à portée de main il n'y avait qu'un ordinateur portable Linux, je voulais faire toute la piste en VCV. Et en cours de route, il y avait un désir de contrôler les synthétiseurs à l'aide du contrôleur midi de manière à ce que l'utilisation des modules disponibles ne fonctionne pas. Au final, j'ai décidé d'écrire un plugin pour connecter Ableton Push 2 au VCV Rack. Nous parlerons de ce qui en est résulté et comment écrire nos propres modules pour VCV dans cet article.

Référence rapide
VCV Rack — open source , . VCV , .


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



API VCV Rack


Chaque module dans VCV se compose de deux parties - audio et graphique. La partie audio hérite de la classe Module la méthode de processus , appelée pour chaque échantillon, c'est-à-dire avec une fréquence d'échantillonnage. Soit dit en passant, la fréquence d'échantillonnage dans VCV peut varier de 44,1 kHz standard à 768 kHz, ce qui permet d'émuler plus précisément des synthétiseurs modulaires en présence d'une puissance de calcul suffisante.

Les objets de type ModuleWidget sont responsables des graphiques , qui héritent de la méthode de dessin de la structure de base . VCV utilise la bibliothèque de graphiques vectoriels nanovg. Dessin à l'intérieur de la méthode de dessinil peut se produire à la fois à l'intérieur des limites du module (l'excès est coupé par le moteur), et par exemple dans le framebuffer, que nous utilisons toujours.

Alors, que faut-il pour écrire votre module pour VCV?

Configuration de l'environnement et installation du SDK en rack


La première étape est bien décrite dans la documentation et ne pose pas de difficultés (au moins sous linux), donc nous ne nous attarderons pas dessus.

Générer un modèle de plugin


Il existe un script helper.py pour cela dans le SDK de rack. Il doit dire createplugin, puis spécifier le nom du plugin et, éventuellement, des informations à son sujet.

<Rack SDK folder>/helper.py createplugin MyPlugin

Lorsque le modèle de plugin est créé, il peut être compilé et installé avec la commande

RACK_DIR=<Rack SDK folder> make install

Dessinez le frontend du module


Chaque plugin peut contenir plusieurs modules, et pour chacun des modules, vous devez dessiner un panneau principal. Pour cela, la documentation VCV nous propose d'utiliser Inkscape ou tout autre éditeur vectoriel. Étant donné que les modules en VCV sont montés sur un support Eurorack virtuel, leur hauteur est toujours de 128,5 mm et la largeur doit être un multiple de 5,08 mm.
Les éléments d'interface de base, tels que les prises CV / Gate, les boutons et les ampoules peuvent être marqués sous forme vectorielle. Pour ce faire, dessinez à leur place des cercles des couleurs correspondantes ( plus de détails ici ), afin que helper.py génère ensuite du code pour ce balisage. Personnellement, il me semble que ce n'est pas une fonctionnalité très pratique et qu'il est plus facile de placer des éléments directement à partir du code. Lorsque l'image et la mise en page sont prêtes, vous devez réexécuter helper.py pour créer un modèle de module et l'associer au panneau avant.

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

Nous connectons les boutons et les torsions




En dehors de l'écran, Ableton Push 2 est considéré par l'ordinateur comme un périphérique USB-MIDI standard, ce qui facilite l'établissement d'une connexion entre celui-ci et VCV Rack. Pour ce faire, créez une file d'attente midi d'entrée et un port midi de sortie dans la classe du module.

Code d'initialisation
struct AbletonPush2 : Module {
    midi::Output midiOutput;
    midi::InputQueue midiInput;

    bool inputConnected;
    bool outputConnected;
}


Essayons de trouver Ableton Push parmi les appareils midi connectés et connectons-nous. Étant donné que le module est conçu pour fonctionner exclusivement avec Ableton Push, nous n'avons pas besoin de charger l'utilisateur d'un choix d'appareil et vous pouvez simplement le trouver par son nom.

Code de connexion du contrôleur
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;
        }
    }
}


Maintenant, dans la méthode de processus , vous pouvez vérifier périodiquement si le contrôleur est connecté et demander à la file d'attente midi si des messages sont arrivés. Ici, il convient de mentionner quoi et comment est généralement codé dans le standard midi. En fait, il existe deux principaux types de messages, ceux-ci sont Note ON / OFF, transmettant le numéro de note et appuyant sur la force, et CC - Command Control, transmettant la valeur numérique d'un paramètre variable. Vous trouverez plus d'informations sur midi ici .

Code d'interrogation de file d'attente 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);
	}
    }
}


Maintenant, je veux apprendre aux pads de contrôleur à briller de différentes couleurs, afin qu'il soit plus pratique de les parcourir. Pour ce faire, nous devrons lui envoyer la commande midi appropriée. Considérez, par exemple, le pad numéro 36 (c'est le pad le plus bas à gauche). Si vous cliquez dessus, le contrôleur enverra la commande 0x90 (note activée), suivie du numéro de note (36) et d'un nombre de 0 à 127, ce qui signifie la force de la presse. Et lorsque, au contraire, l'ordinateur envoie la même commande 0x90 et le même numéro de note 36 au contrôleur, alors le troisième chiffre indiquera la couleur avec laquelle le pavé inférieur gauche doit briller. Les numéros des boutons et des pads sont indiqués dans la figure ci-dessus. Push a plusieurs possibilités pour travailler avec la couleur, les palettes, l'animation et d'autres paramètres de rétro-éclairage. Je ne suis pas entré dans les détails et j'ai simplement apporté toutes les couleurs possibles de la palette par défaut aux pads et j'ai choisi celles que j'aimais.Mais dans le cas général, la conversion des commandes midi en valeurs PWM sur les LED, selon la documentation, ressemble à ceci:



Code de contrôle du rétroéclairage
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);
}


Nous connectons l'écran




Pour connecter l'écran, vous devez travailler avec USB. Le contrôleur lui-même ne sait rien dessiner et ne sait rien des graphiques. Tout ce qu'il veut, c'est envoyer un cadre de 160x960 pixels au moins toutes les 2 secondes. Tout le rendu a lieu sur le côté de l'ordinateur. Pour démarrer, connectez et ouvrez le périphérique USB, comme décrit dans la documentation :

Afficher le code de connexion
#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);
}


Pour transmettre une trame, vous devez d'abord envoyer un en-tête de 16 octets, puis 160 lignes de 960 nombres de 16 bits chacune. Dans le même temps, selon la documentation, les chaînes ne devraient pas être transmises en 1920 octets, mais en paquets de 2048 octets, complétés par des zéros.

Code de transfert de trame
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);
    }
}


Maintenant, il ne reste plus qu'à dessiner et écrire une image dans le tampon. Pour ce faire, utilisez la FramebufferWidget classe qui implémente la drawFramebuffer méthode . VCV Rack prêt à l' emploi utilise la bibliothèque nanovg , il est donc assez facile d'écrire des graphiques ici. Nous apprenons le contexte graphique actuel à partir de la variable globale APP et enregistrons son état. Ensuite, créez un cadre vide de 160 x 960 et dessinez-le avec la méthode de dessin . Après cela, copiez le framebuffer dans le tableau qui sera envoyé via USB et retournez l'état du contexte. À la fin, définissez le drapeau sale pour qu'à la prochaine itération du rendu, le moteur VCV n'oublie pas de mettre à jour notre widget.

Code de rendu d'image
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;
}


Logique d'interaction et cartographie des paramètres


Pour ma tâche, je voulais pouvoir changer de motif enregistré dans des séquenceurs, et en même temps contrôler un certain ensemble de paramètres, le mien pour chaque groupe de motifs. En mappant, je comprends la liaison du paramètre d'un module avec le paramètre d'un autre module ou contrôleur midi. J'ai trouvé très peu d'informations sur la façon de rendre le mappage magnifique, alors j'ai pris la plupart du code qui l'implémente du module MIDI Map intégré dans VCV . En bref, pour chaque groupe de paramètres, un objet ParamHandle spécial y est créé , qui indique au moteur à l' aide de béquilles ce qui est connecté à quoi.

Conclusion


Voici un module que j'ai finalement obtenu. En plus de la conversion standard de midi en CV, il vous permet de grouper les pads, de leur assigner des couleurs et d'associer des paramètres arbitraires de modules aux encodeurs Push, en affichant leurs noms et valeurs sur l'écran du contrôleur. Dans la vidéo ci-dessous, vous pouvez voir son aperçu, et dans cette vidéo, vous pouvez voir en action.


Le code complet est disponible ici .

Nous avons réussi à le tester uniquement sous Linux (Ubuntu 18.04), sur MacOS l'affichage ne se connectait pas en raison des spécificités de libusb et il y avait d'étranges retards dans l'interface midi. Malgré cela, VCV Rack a laissé une très bonne impression à la fois en tant que framework et en tant que DAW modulaire, j'espère que VCV continuera à se développer, et cet article aidera quelqu'un d'autre à écrire ses propres modules pour cela.

All Articles