首页 > 解决方案 > 在 macOS 上将 MIDI 控制输入转换为虚拟按键(或虚拟 USB 按钮等)

问题描述

我想使用 MIDI 控制设备(如https://www.korg.com/us/products/computergear/nanokontrol2/)为各种软件生成控制输入,尤其是 Blender。

一种方法显然是将 MIDI 输入处理添加到 Blender 中。向 Blender 添加低级代码来监听 MIDI 按钮和滑块一点也不难,我基本上已经实现了。(即,我在 Blender 的最低级别添加了一个新的输入“类”,MIDI。)但是将其连接到现有的键盘和鼠标管道,尤其是 UI 功能以将功能与输入相关联要复杂得多,这不是我想要的现在潜入。

另一种方法可能是运行一些单独的软件来监听 MIDI 事件并将其转换为虚拟击键。假设可以生成比任何键盘上的实际键更多的击键种类,这可以很好地工作(例如,生成与真正键盘所没有的各种 Unicode 块相对应的击键)。这听起来可行吗?实现这种虚拟击键生成我应该考虑使用 a11y API 吗?这种方式的好处是它可以与任何软件一起使用。

或者有人有更好的主意吗?

标签: macosblendermidi

解决方案


好的,所以我写了这个小程序。运行良好(一旦您授予它在系统偏好设置>安全和隐私>隐私>辅助功能>允许下面的应用程序控制您的计算机中生成关键事件的权利)。MIDI 音符开和关事件以及 MIDI 控制器值更改生成 macOS 按键,以 CJK 统一表意文字作为字符。

但是,后来我看到 Blender 是一种认为 ASCII 对每个人都应该足够的软件。换句话说,Blender 有硬编码限制,它处理的唯一键基本上是英文键盘上的键。您甚至无法将西里尔文或希腊语键(毕竟存在实际键盘)绑定到 Blender 功能,更不用说 CJK 键了。叹。回到绘图板。

/* -*- Mode: ObjC; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; fill-column: 150 -*- */

#import <array>
#import <cassert>
#import <cstdio>

#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
#import <CoreMidi/CoreMidi.h>

static constexpr bool DEBUG_MIDI2KBD(true);

static constexpr int BASE_KEYCODE(1000);

static CGEventSourceRef eventSource;

static std::array<unsigned char, 16*128> control;

static void NotifyProc(const MIDINotification *message, void *refCon)
{
}

static void sendKeyDownOrUpEvent(int character, int velocity, bool down) {
  CGEventRef event = CGEventCreateKeyboardEvent(eventSource, character + BASE_KEYCODE, down);

  // We send CJK Unified Ideographs characters
  constexpr int START = 0x4E00;
  assert(character >= 0 && character <= 20989);

  const UniChar string[1] = { (UniChar)(START + character) };

  CGEventKeyboardSetUnicodeString(event, 1, string);

  CGEventPost(kCGAnnotatedSessionEventTap, event);
}

int main(int argc, const char * argv[]) {
  @autoreleasepool {
    MIDIClientRef midi_client;
    OSStatus status = MIDIClientCreate((__bridge CFStringRef)@"MIDI2Kbd", NotifyProc, nullptr, &midi_client);
    if (status != noErr) {
      fprintf(stderr, "Error %d while setting up handlers\n", status);
      return 1;
    }

    eventSource = CGEventSourceCreate(kCGEventSourceStatePrivate);

    control.fill(0xFF);

    ItemCount number_sources = MIDIGetNumberOfSources();
    for (int i = 0; i < number_sources; i++) {
      MIDIEndpointRef source = MIDIGetSource(i);
      MIDIPortRef port;
      status = MIDIInputPortCreateWithProtocol(midi_client,
                                               (__bridge CFStringRef)[NSString stringWithFormat:@"MIDI2Kbd input %d", i],
                                               kMIDIProtocol_1_0,
                                               &port,
                                               ^(const MIDIEventList *evtlist, void *srcConnRefCon) {
                                                 const MIDIEventPacket* packet = &evtlist->packet[0];

                                                 for (int i = 0; i < evtlist->numPackets; i++) {
                                                   // We expect just MIDI 1.0 packets.
                                                   // The words are in big-endian format.
                                                   assert(packet->wordCount == 1);

                                                   const unsigned char *bytes = reinterpret_cast<const unsigned char *>(&packet->words[0]);
                                                   assert(bytes[3] == 0x20);

                                                   if (DEBUG_MIDI2KBD)
                                                     printf("Event: %02X %02X %02X\n", bytes[2], bytes[1], bytes[0]);

                                                   switch ((bytes[2] & 0xF0) >> 4) {
                                                   case 0x9: // Note-On
                                                     assert(bytes[1] <= 0x7F);
                                                     sendKeyDownOrUpEvent((bytes[2] & 0x0F) * 128 + bytes[1], bytes[0], true);
                                                     break;

                                                   case 0x8: // Note-Off
                                                     assert(bytes[1] <= 0x7F);
                                                     sendKeyDownOrUpEvent((bytes[2] & 0x0F) * 128 + bytes[1], bytes[0], false);
                                                     break;

                                                   case 0xB: // Control Change
                                                     assert(bytes[1] <= 0x7F);
                                                     const int number = (bytes[2] & 0x0F) * 128 + bytes[1];
                                                     if (control.at(number) != 0xFF) {
                                                       int diff = bytes[0] - control.at(number);

                                                       // If it switches from 0 to 127 or back, we assume it is not really a continuous controller but
                                                       // a button.

                                                       if (diff == 127)
                                                         diff = 1;
                                                       else if (diff == -127)
                                                         diff = -1;

                                                       if (diff > 0) { 
                                                         for (int i = 0; i < diff; i++) {
                                                           // Send keys indicating single-step control value increase
                                                           sendKeyDownOrUpEvent(16*128 + number * 2, diff, true);
                                                           sendKeyDownOrUpEvent(16*128 + number * 2, diff, false);
                                                         }
                                                       } else if (diff < 0) {
                                                         for (int i = 0; i < -diff; i++) {
                                                           // Send key indicating single-step control value decrease
                                                           sendKeyDownOrUpEvent(16*128 + number * 2 + 1, -diff, true);
                                                           sendKeyDownOrUpEvent(16*128 + number * 2 + 1, -diff, false);
                                                         }
                                                       }
                                                     }
                                                     control.at(number) = bytes[0];
                                                     break;
                                                   }

                                                   packet = MIDIEventPacketNext(packet);
                                                 }
                                               });
      if (status != noErr) {
        fprintf(stderr, "Error %d while setting up port\n", status);
        return 1;
      }
      status = MIDIPortConnectSource(port, source, nullptr);
      if (status != noErr) {
        fprintf(stderr, "Error %d while connecting port to source\n", status);
        return 1;
      }
    }
    CFRunLoopRun();
  }
  return 0;
}

推荐阅读