O Ableton não é necessário: conecte o Ableton Push 2 ao rack de VCV


Ultimamente, a criação da música acontece da mesma maneira que uma fotografia há 10 anos: cada uma tem sua própria conta DSLR e instagram. A indústria da música está muito feliz com isso, porque esse interesse gera muito dinheiro. Todos os dias surgem novos plugins VST e dispositivos analógicos, o número de cursos temáticos está crescendo rapidamente, blogs dedicados à produção musical vão para o topo do youtube. Nesse contexto, os projetos de código aberto parecem muito interessantes, permitindo que qualquer pessoa que queira se experimentar como produtor sem gastar muito dinheiro com isso. Um desses projetos é o VCV Rack, que visa tornar a classe mais cara de instrumentos musicais - sintetizadores modulares analógicos - acessível a todos. Recentemente, quando tive a ideia de uma faixa,e na mão havia apenas um laptop linux, eu queria fazer a faixa inteira em VCV. E ao longo do caminho, houve um desejo de controlar os sintetizadores usando o controlador midi de uma maneira que o uso dos módulos disponíveis não funcionasse. No final, decidi escrever um plugin para conectar o Ableton Push 2 ao VCV Rack. Falaremos sobre o que aconteceu e como escrever nossos próprios módulos para VCV neste artigo.

Referência rápida
VCV Rack — open source , . VCV , .


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



API de rack de VCV


Cada módulo no VCV consiste em duas partes - áudio e gráfico. A parte de áudio herda da classe Module o método de processo , chamado para cada amostra, ou seja, com uma frequência de amostragem. A propósito, a frequência de amostragem no VCV pode variar de 44,1 kHz padrão a 768 kHz, o que permite emulação mais precisa de sintetizadores modulares com capacidade de computação suficiente.

Objetos do tipo ModuleWidget são responsáveis ​​pelos gráficos , que herdam o método draw da estrutura base . O VCV usa a biblioteca de gráficos vetoriais nanovg. Desenho dentro do método drawisso pode ocorrer dentro dos limites do módulo (o excesso é cortado pelo mecanismo) e, por exemplo, no buffer de estrutura, que ainda usamos.

Então, o que é necessário para escrever seu módulo para VCV?

Configurando o Ambiente e Instalando o Rack SDK


O primeiro passo é bem descrito na documentação e não causa nenhuma dificuldade (pelo menos no linux), portanto, não iremos nos debruçar sobre ele.

Gere um modelo de plug-in


Há um script helper.py para isso no Rack SDK. Ele precisa dizer createplugin e depois especificar o nome do plug-in e, opcionalmente, informações sobre ele.

<Rack SDK folder>/helper.py createplugin MyPlugin

Quando o modelo do plugin é criado, ele pode ser compilado e instalado com o comando

RACK_DIR=<Rack SDK folder> make install

Desenhe a interface do módulo


Cada plug-in pode conter vários módulos e, para cada um dos módulos, você precisa desenhar um painel principal. Para isso, a documentação do VCV nos oferece o uso do Inkscape ou de qualquer outro editor de vetores. Como os módulos no VCV são montados em um suporte virtual Eurorack, sua altura é sempre 128,5 mm e a largura deve ser um múltiplo de 5,08 mm.
Elementos básicos da interface, como soquetes CV / Gate, botões e lâmpadas, podem ser marcados em forma de vetor. Para fazer isso, desenhe em seu lugar círculos das cores correspondentes ( mais detalhes aqui ), para que helper.py gere código para essa marcação. Pessoalmente, pareceu-me que esse não é um recurso muito conveniente e é mais fácil colocar elementos diretamente do código. Quando a imagem e o layout estiverem prontos, você precisará executar o helper.py novamente para criar um modelo de módulo e associá-lo ao painel frontal.

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

Conectamos botões e torções




Além da tela, o Ableton Push 2 é visto pelo computador como um dispositivo USB-MIDI comum, o que facilita o estabelecimento de uma conexão entre ele e o rack de VCV. Para fazer isso, crie uma fila midi de entrada e uma porta midi de saída dentro da classe do módulo.

Código de inicialização
struct AbletonPush2 : Module {
    midi::Output midiOutput;
    midi::InputQueue midiInput;

    bool inputConnected;
    bool outputConnected;
}


Vamos tentar encontrar o Ableton Push entre os dispositivos midi conectados e conectar a ele. Como o módulo foi projetado para funcionar exclusivamente com o Ableton Push, não precisamos sobrecarregar o usuário com a escolha do dispositivo e você pode simplesmente encontrá-lo pelo nome.

Código de conexão do 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;
        }
    }
}


Agora, no método de processo , você pode verificar periodicamente se o controlador está conectado e perguntar à fila do midi se alguma mensagem chegou. Aqui vale a pena mencionar o que e como é geralmente codificado no padrão midi. De fato, existem dois tipos principais de mensagens: Nota ON / OFF, transmitindo o número da nota e pressionando a força, e CC - Command Control, transmitindo o valor numérico de algum parâmetro variável. Mais informações sobre o midi podem ser encontradas aqui .

Código de pesquisa da fila 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);
	}
    }
}


Agora, quero ensinar os controles a brilharem em cores diferentes, para que seja mais conveniente navegá-los. Para fazer isso, precisaremos enviar a ele o comando midi apropriado. Considere, por exemplo, o teclado número 36 (este é o teclado esquerdo mais baixo). Se você clicar nele, o controlador enviará o comando 0x90 (nota ativada), seguido pelo número da nota (36) e um número de 0 a 127, o que significa a força da impressora. E quando, pelo contrário, o computador envia ao controlador o mesmo comando 0x90 e o mesmo número de nota 36, ​​o terceiro número indica a cor com a qual o teclado inferior esquerdo deve brilhar. Os números dos botões e botões são mostrados na figura acima. O Push tem algumas possibilidades para trabalhar com cores, paletas, animação e outros parâmetros de luz de fundo. Não entrei em detalhes e simplesmente trouxe todas as cores possíveis da paleta padrão para os blocos e escolhi as que eu mais gostava.Mas, no caso geral, a conversão de comandos midi em valores de PWM em LEDs, de acordo com a documentação, é assim:



Código de controle da luz de fundo
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);
}


Nós conectamos a tela




Para conectar a tela, você precisa trabalhar com USB. O próprio controlador não sabe desenhar nada e não sabe nada sobre gráficos. Tudo o que ele quer é enviar um quadro de 160x960 pixels em tamanho pelo menos a cada 2 segundos. Toda a renderização ocorre na lateral do computador. Para iniciar, conecte e abra o dispositivo USB, conforme descrito na documentação :

Exibir código de conexão
#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 um quadro, você deve primeiro enviar um cabeçalho de 16 bytes e, em seguida, 160 linhas de 960 números de 16 bits cada. Ao mesmo tempo, de acordo com a documentação, as strings não devem ser transmitidas em 1920 bytes, mas em pacotes de 2048 bytes, complementados com zeros.

Código de transferência de quadros
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);
    }
}


Agora resta apenas desenhar e gravar um quadro no buffer. Para fazer isso, use a classe FramebufferWidget que implementa o método drawFramebuffer . O rack VCV pronto para uso usa a biblioteca nanovg , por isso é muito fácil escrever gráficos aqui. Aprendemos o contexto gráfico atual da variável global APP e salvamos seu estado. Em seguida, crie um quadro vazio de 160x960 e desenhe-o com o método draw . Depois disso, copie o framebuffer para a matriz que será enviada via USB e retorne o estado do contexto. No final, defina o sinalizador sujo para que, na próxima iteração da renderização, o mecanismo VCV não esqueça de atualizar nosso widget.

Código de renderização de quadro
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 interação e mapeamento de parâmetros


Para minha tarefa, eu queria poder alternar padrões gravados em seqüenciadores e, ao mesmo tempo, controlar um certo conjunto de parâmetros, o meu para cada grupo de padrões. Por mapeamento, entendo a ligação do parâmetro de um módulo com o parâmetro de outro módulo ou controlador midi. Encontrei muito pouca informação sobre como fazer um mapeamento bonito, então peguei a maior parte do código que o implementa a partir do módulo MIDI Map embutido no VCV . Em resumo, para cada grupo de parâmetros, um objeto ParamHandle especial é criado lá , que informa ao mecanismo através de muletas o que está conectado com o que.

Conclusão


Aqui está um módulo que recebi no final. Além da conversão padrão de midi para CV, permite agrupar pads, atribuir cores a eles e associar parâmetros arbitrários de módulos a codificadores Push, exibindo seus nomes e valores no visor do controlador. No vídeo abaixo, você pode ver sua visão geral e, neste vídeo, você pode ver em ação.


O código completo está disponível aqui .

Conseguimos testá-lo apenas no Linux (Ubuntu 18.04), no MacOS o monitor não se conectou devido às especificidades do libusb e houve atrasos estranhos na interface midi. Apesar disso, o VCV Rack deixou uma impressão muito boa como estrutura e como DAW modular, espero que o VCV continue a se desenvolver, e este artigo ajude alguém a escrever seus próprios módulos.

All Articles