Pull to refresh

Использование синтезатора в качестве компьютерной клавиатуры

Reading time 5 min
Views 13K
Недавно мне в голову пришла мысль: а нельзя ли, подключив синтезатор к компьютеру, набирать на нем текст? Я попробовал реализовать это, и у меня получилось. Моя программа считывает нажатия клавиш синтезатора и эмулирует нажатия клавиш обычной клавиатуры. В этой статье я расскажу, как это реализовать. Писать будем под Linux на C++ с использованием Qt.




Чтение данных из синтезатора


Итак, в наличии имеется ноутбук с Linux и синтезатор Yamaha DGX-200. Подключаем синтезатор через USB-разъем к ноутбуку и видим, что устройство распозналось:



Из устройства идет постоянный поток вопросиков, среди которых появляются другие символы при нажатии клавиш синтезатора. Кстати, интересный факт: если записать этот вывод в файл, а потом прочитать из файла и записать обратно в /dev/midi2, то синтезатор через свои колонки воспроизведет те ноты, которые были нажаты при записи, но без пауз.

Следующая задача — разбор этого потока. После продолжительных поисков в гугле я решил воспользоваться библиотекой portmidi. Документация к ней довольно скудная, рабочих примеров я вообще не нашел. Что ж, теперь будет одним примером больше. Получаем список устройств:
int count = Pm_CountDevices();
for(int i = 0; i < count; i++) {
  const PmDeviceInfo* info = Pm_GetDeviceInfo(i);
  qDebug() << i << ": " << info->name 
           <<  " input: " << info->input
           << " output: " << info->output;
}

У меня получился такой результат:
0: Midi Through Port-0 input: 0 output: 1
1: Midi Through Port-0 input: 1 output: 0
2: YAMAHA Portable G MIDI 1 input: 0 output: 1
3: YAMAHA Portable G MIDI 1 input: 1 output: 0

Для дальнейшей работы с устройством нам потребуется знать только id, который указан в начале строки. Нам подходит устройство 3 — входной (input=1) поток от нашего синтезатора. Открываем нужный поток:
PortMidiStream* stream = 0;
PmError e = Pm_OpenInput(&stream, good_id, 0, 100, 0, 0);
if (e != pmNoError) {
  qWarning() << "Can't open input, error: " << e << endl;
  return 2;
}

После этого периодически читаем данные. Я использовал Qt-слот с периодическим вызовом по таймеру, но подойдет и обычный while(true) и sleep.
PmEvent event; // структура, в которую будут записаны пришедшие данные
int c = Pm_Read(stream, &event, 1); // читаем одно сообщение из устройства
if (c > 0 && Pm_MessageStatus(event.message) == 144) {
  unsigned int note = Pm_MessageData1(event.message),
               volume = Pm_MessageData2(event.message);
  // дальнейшая обработка note и volume 
}

Чтобы пояснить, что это за магические числа, я расскажу, как устроены MIDI-команды.

MIDI-команды


Каждое сообщение (оно же MIDI-команда) состоит из трех целых чисел, которые в portmidi называются status, data1, data2. Таблицу с возможными статусами можно посмотреть здесь. Нас интересует только статус 144 — изменение состояния ноты на первом канале. В data1 при этом передается номер ноты, а в data2 — ее громкость. Например, когда вы нажимаете клавишу «до» первой октавы на синтезаторе, приходит команда 144 60 95, а когда отпускаете — 144 60 0.

Громкость варьируется от 0 до 127 и зависит от силы удара по клавише. Теоретически можно выводить заглавные буквы вместо строчных, когда пользователь сильно бьет по клавиатуре. Ну, а номер ноты — это просто порядковый номер, соответствие нот номерам можно посмотреть на этой картинке:

Обозначение нот и аккордов


Я решил обозначать ноту как «3C» или «3C#», где 3 — номер октавы (причем октава начинается с «ля», так проще), C — обозначение ноты («до»), а при необходимости добавляется диез. Вот как это реализовано:
class Note {
public:
  Note(int midi_number);
  QString to_string() const;
  int tone, octave;
};

Note::Note(int midi_number) {
  int n = midi_number - 21;
  octave = n / 12;
  tone = (n - octave * 12);
}

QString Note::to_string() const {
  return QObject::tr("%1%2").arg(octave).arg(
    tone == 0?  "A":
    tone == 1?  "A#":
    tone == 2?  "B":
    tone == 3?  "C":
    tone == 4?  "C#":
    tone == 5?  "D":
    tone == 6?  "D#":
    tone == 7?  "E":
    tone == 8?  "F":
    tone == 9?  "F#":
    tone == 10? "G":
    tone == 11? "G#": "??"
  );
}

Если пользователь нажимает аккорд (несколько нот одновременно), то приходит несколько сообщений почти сразу. Мы можем программно отслеживать эту ситуацию и отличать одиночные нажатия от аккордов. В моей программе можно ставить в соответствие различным буквам различные аккорды. Чтобы получить обозначение аккорда, соединим плюсом обозначения входящих в него клавиш: «3C#+3E+3G#». Когда пользователь нажимает ноту или аккорд, программа ищет в раскладке строку, совпадающую с этим обозначением, и эмулирует нажатие соответствующей клавиши. Когда клавиша на синтезаторе отпускается, эмулируется отпускание клавиши. Модификаторы (Shift, Ctrl и т.д.) здесь не отличаются от других клавиш. Все сочетания работают, как положено.

Эмуляция нажатий клавиш


Ура, мы умеем определять, когда ноты нажимают и отпускают. Теперь научимся эмулировать нажатие клавиш клавиатуры. Будем использовать решение, которое я нашел на Stack Overflow.
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/extensions/XTest.h>

Display* display = XOpenDisplay(0);

void emulate_key(QString key, bool pressed) {
  KeySym sym = XStringToKeysym(key.toAscii());
  if (sym == NoSymbol) {
    qWarning() << "Failed to emulate key: " << key;
    return;
  }
  XTestFakeKeyEvent(display, XKeysymToKeycode(display, sym), pressed, 0);
  XFlush(display);
}

Я добавил использование функции XStringToKeysym, чтобы эмулировать клавишу по ее имени, которое мы будем брать из конфигурационного файла. Список допустимых клавиш можно найти в заголовочном файле /usr/include/X11/keysymdef.h.

Раскладку будем хранить в файле layout.ini следующего вида:
; letters
X = 3C#+3G#
Y = 3D#+3G#
Z = 3E+3G#

; navigation
Space     = 2E
Return    = 1A+2A
BackSpace = 4C#
Delete    = 4D#
Left      = 4A
Right     = 4C

Раскладка


Осталась последняя задача — придумать раскладку. Простой вариант — каждой букве по клавише — оказался неудобным. Клавиш хватает, но впритык, к тому же приходится часто переносить руки из-за больших габаритов синтезатора. К счастью, мы можем использовать комбинации клавиш, причем на синтезаторе их нажимать гораздо удобнее, чем на клавиатуре компьютера.

Попробуем уместить на 12 клавишах (от A до G#) 26 латинских букв. Выбор букв для семи белых клавиш очевиден — это буквы от A до G, являющиеся общепринятыми обозначениями соответствующих нот. Затем я выписал оставшиеся буквы в порядке убывающей частоты употребления и попробовал составить слово из букв, находящихся ближе к началу списка. У меня получилось слово HINTS, и я отдал этим буквам пять черных клавиш. Остальным клавишам я в алфавитном порядке назначил большие и малые терции внутри той же октавы. Осталась еще куча вариантов для размещения других букв (например, русских).

Остальным клавишам тоже нашлось место на клавиатуре. У меня получилась такая раскладка:



Использовался редактор нотной записи MuseScore.

На видео, приложенном к посту, продемонстрирован набор программы Hello world и ее компиляция с использованием синтезатора вместо клавиатуры.

Код программы выложен на Github.
Tags:
Hubs:
+66
Comments 92
Comments Comments 92

Articles