最近,音乐创作与十年前的照片大致相同:每个人都有自己的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
#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];
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;
}
}
}
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);
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能够继续发展,并且本文将帮助其他人为此编写自己的模块。