不需要Ableton:将Ableton Push 2连接到VCV机架


最近,音乐创作与十年前的照片大致相同:每个人都有自己的DSLR和instagram帐户。音乐界对此很高兴,因为这种兴趣带来了很多钱。每天都有新的VST插件和模拟设备出现,主题课程的数量在迅速增长,致力于音乐制作的博客在YouTube上排名第一。在这种背景下,开源项目看起来非常有趣,允许任何想要尝试自己作为生产者的人都不必为此花很多钱。 VCV Rack是这样的一个项目,该项目旨在使所有人都能使用最昂贵的一类乐器-模拟模块化合成器。最近,当我想到一个曲目的想法时,当时只有一台linux笔记本电脑,我想用VCV进行整个开发。一直以来,都希望使用midi控制器以无法使用可用模块的方式来控制合成器。最后,我决定编写一个插件以将Ableton Push 2连接到VCV Rack。在本文中,我们将讨论它的来龙去脉,以及如何编写自己的VCV模块。

快速参考
VCV Rack — open source , . VCV , .


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



VCV机架API


VCV中的每个模块都包含两个部分-音频和图形。音频部分从Module类继承process方法过程方法针对每个样本进行调用,即具有采样频率。顺便说一下,VCV中的采样频率可以从标准的44.1 kHz到高达768 kHz不等,这允许具有足够计算能力的模块化合成器更精确地仿真。ModuleWidget

类型的对象负责图形,这些图形从基本结构继承了draw方法VCV使用nanovg矢量图形库。draw方法内绘图它既可以在模块边界内发生(多余的被引擎切断),也可以在例如我们仍然使用的帧缓冲区内发生。

那么,为VCV编写模块又需要什么呢?

设置环境并安装Rack SDK


第一步在文档中已得到很好的描述,并且不会造成任何困难(至少在linux下),因此我们不再赘述。

生成插件模板


在Rack SDK中有一个helper.py脚本。他需要说出createplugin,然后指定插件的名称以及有关它的信息(可选)。

<Rack SDK folder>/helper.py createplugin MyPlugin

创建插件模板后,可以使用以下命令对其进行编译和安装

RACK_DIR=<Rack SDK folder> make install

绘制模块的前端


每个插件可以包含多个模块,并且对于每个模块,您都需要绘制一个主面板。为此,VCV文档为我们提供了使用Inkscape或任何其他矢量编辑器的功能。由于VCV中的模块安装在虚拟Eurorack支架上,因此它们的高度始终为128.5 mm,宽度应为5.08 mm的倍数。
基本接口元素,例如CV /门插座,按钮和灯泡,可以矢量形式标记。为此,请绘制其相应颜色的位置圆(此处更多详细信息),以便helper.py为该标记生成代码。就个人而言,在我看来这不是一个非常方便的功能,直接从代码中放置元素更容易。图片和布局准备就绪后,您需要再次运行helper.py 创建模块模板并将其与前面板关联。

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

我们连接按钮和曲折




除了显示屏之外,Ableton Push 2在计算机上还被视为普通的USB-MIDI设备,因此可以轻松地在其与VCV机架之间建立连接。为此,请在模块类内创建一个输入Midi队列和一个输出Midi端口。

初始化代码
struct AbletonPush2 : Module {
    midi::Output midiOutput;
    midi::InputQueue midiInput;

    bool inputConnected;
    bool outputConnected;
}


让我们尝试在连接的Midi设备中查找Ableton Push并将其连接。由于该模块专用于Ableton Push,因此我们无需为用户选择设备负担,您只需按名称查找即可。

控制器连接码
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;
        }
    }
}


现在,在处理方法中,您可以定期检查控制器是否已连接,并询问Midi队列是否有任何消息到达。在这里值得一提的是,MIDI标准中通常编码什么以及如何编码。实际上,消息有两种主要类型,它们是“音符开/关”,传输音符编号和按压力,以及“ CC-命令控制”,传输某些可变参数的数值。有关midi的更多信息,请参见此处

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);
	}
    }
}


现在,我要教控制器垫以不同的颜色发光,以便更轻松地浏览它们。为此,我们需要向他发送适当的midi命令。例如,考虑打击垫编号36(这是最低的左侧打击垫)。如果单击它,控制器将发送命令0x90(注解开),后跟注解号(36)和0到127之间的数字,这表示压机的强度。相反,当计算机向控制器发送相同的0x90命令和相同的音符编号36时,则第三个数字将指示左下垫发光的颜色。按键和打击垫的编号如上图所示。 Push具有使用颜色,调色板,动画和其他背光参数的多种可能性。我没有详细介绍,只是将默认调色板的所有可能的颜色带到了垫板上,然后选择了我喜欢的颜色。但是在一般情况下,根据文档将midi命令转换为LED上的PWM值看起来像这样:



背光控制码
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);
}


我们连接显示器




要连接显示器,您必须使用USB。控制器本身不知道如何绘制任何内容,也不了解图形。他只希望至少每2秒发送一个160x960像素的帧。所有渲染都在计算机侧面进行。首先,按照文档中的说明连接并打开USB设备

显示连接码
#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);
}


要发送帧,必须首先发送一个16字节的标头,然后发送160行,每行960个16位数字。同时,根据文档,字符串不应该以1920字节传输,而应该以2048字节的数据包传输,并以零进行补充。

帧传输码
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);
    }
}


现在只剩下绘制帧并将其写入缓冲区。为此,请使用实现drawFramebuffer方法FramebufferWidget开箱即用的VCV Rack使用nanovg,因此在此处编写图形非常容易。我们从全局变量APP中了解当前的图形上下文并保存其状态。接下来,创建一个空的160x960框架,并使用draw方法进行绘制之后,将帧缓冲区复制到将通过USB发送的阵列,然后返回上下文状态。最后,设置dirty标志,以便在下一次渲染渲染时,VCV引擎不会忘记更新我们的小部件。

帧渲染代码
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;
}


交互逻辑和参数映射


对于我的任务,我希望能够切换定序器中记录的模式,并同时控制每组模式自己的一组特定参数。通过映射,我了解了一个模块的参数与另一模块或Midi控制器的参数的绑定。我发现很少有关于如何进行精美映射的信息,因此我从VCV内置的MIDI Map模块中获取了实现该映射的大部分代码简而言之,对于每组参数,都会其中创建一个特殊的ParamHandle对象,该对象通过拐杖告诉引擎什么与什么相关。

结论


这是我最后得到的一个模块。除了将midi转换为CV的标准方法外,它还允许您对打击垫进行分组,为其分配颜色,以及将任意模块参数与Push编码器相关联,在控制器显示屏上显示其名称和值。在下面的视频中,您可以看到他的概述,在此视频中,您可以看到实际操作。


完整代码可在此处获得

我们设法仅在Linux(Ubuntu 18.04)下对其进行测试,在MacOS上,由于libusb的特性导致显示器无法连接,并且midi界面出现了奇怪的延迟。尽管如此,VCV Rack作为框架和模块化DAW都给人留下了很好的印象,我希望VCV能够继续发展,并且本文将帮助其他人为此编写自己的模块。

All Articles