Недавно мне в голову пришла мысль: а нельзя ли, подключив синтезатор к компьютеру, набирать на нем текст? Я попробовал реализовать это, и у меня получилось. Моя программа считывает нажатия клавиш синтезатора и эмулирует нажатия клавиш обычной клавиатуры. В этой статье я расскажу, как это реализовать. Писать будем под Linux на C++ с использованием Qt.
Итак, в наличии имеется ноутбук с Linux и синтезатор Yamaha DGX-200. Подключаем синтезатор через USB-разъем к ноутбуку и видим, что устройство распозналось:

Из устройства идет постоянный поток вопросиков, среди которых появляются другие символы при нажатии клавиш синтезатора. Кстати, интересный факт: если записать этот вывод в файл, а потом прочитать из файла и записать обратно в /dev/midi2, то синтезатор через свои колонки воспроизведет те ноты, которые были нажаты при записи, но без пауз.
Следующая задача — разбор этого потока. После продолжительных поисков в гугле я решил воспользоваться библиотекой portmidi. Документация к ней довольно скудная, рабочих примеров я вообще не нашел. Что ж, теперь будет одним примером больше. Получаем список устройств:
У меня получился такой результат:
Для дальнейшей работы с устройством нам потребуется знать только id, который указан в начале строки. Нам подходит устройство 3 — входной (input=1) поток от нашего синтезатора. Открываем нужный поток:
После этого периодически читаем данные. Я использовал Qt-слот с периодическим вызовом по таймеру, но подойдет и обычный while(true) и sleep.
Чтобы пояснить, что это за магические числа, я расскажу, как устроены MIDI-команды.
Каждое сообщение (оно же MIDI-команда) состоит из трех целых чисел, которые в portmidi называются status, data1, data2. Таблицу с возможными статусами можно посмотреть здесь. Нас интересует только статус 144 — изменение состояния ноты на первом канале. В data1 при этом передается номер ноты, а в data2 — ее громкость. Например, когда вы нажимаете клавишу «до» первой октавы на синтезаторе, приходит команда 144 60 95, а когда отпускаете — 144 60 0.
Громкость варьируется от 0 до 127 и зависит от силы удара по клавише. Теоретически можно выводить заглавные буквы вместо строчных, когда пользователь сильно бьет по клавиатуре. Ну, а номер ноты — это просто порядковый номер, соответствие нот номерам можно посмотреть на этой картинке:

Я решил обозначать ноту как «3C» или «3C#», где 3 — номер октавы (причем октава начинается с «ля», так проще), C — обозначение ноты («до»), а при необходимости добавляется диез. Вот как это реализовано:
Если пользователь нажимает аккорд (несколько нот одновременно), то приходит несколько сообщений почти сразу. Мы можем программно отслеживать эту ситуацию и отличать одиночные нажатия от аккордов. В моей программе можно ставить в соответствие различным буквам различные аккорды. Чтобы получить обозначение аккорда, соединим плюсом обозначения входящих в него клавиш: «3C#+3E+3G#». Когда пользователь нажимает ноту или аккорд, программа ищет в раскладке строку, совпадающую с этим обозначением, и эмулирует нажатие соответствующей клавиши. Когда клавиша на синтезаторе отпускается, эмулируется отпускание клавиши. Модификаторы (Shift, Ctrl и т.д.) здесь не отличаются от других клавиш. Все сочетания работают, как положено.
Ура, мы умеем определять, когда ноты нажимают и отпускают. Теперь научимся эмулировать нажатие клавиш клавиатуры. Будем использовать решение, которое я нашел на Stack Overflow.
Я добавил использование функции XStringToKeysym, чтобы эмулировать клавишу по ее имени, которое мы будем брать из конфигурационного файла. Список допустимых клавиш можно найти в заголовочном файле /usr/include/X11/keysymdef.h.
Раскладку будем хранить в файле layout.ini следующего вида:
Осталась последняя задача — придумать раскладку. Простой вариант — каждой букве по клавише — оказался неудобным. Клавиш хватает, но впритык, к тому же приходится часто переносить руки из-за больших габаритов синтезатора. К счастью, мы можем использовать комбинации клавиш, причем на синтезаторе их нажимать гораздо удобнее, чем на клавиатуре компьютера.
Попробуем уместить на 12 клавишах (от A до G#) 26 латинских букв. Выбор букв для семи белых клавиш очевиден — это буквы от A до G, являющиеся общепринятыми обозначениями соответствующих нот. Затем я выписал оставшиеся буквы в порядке убывающей частоты употребления и попробовал составить слово из букв, находящихся ближе к началу списка. У меня получилось слово HINTS, и я отдал этим буквам пять черных клавиш. Остальным клавишам я в алфавитном порядке назначил большие и малые терции внутри той же октавы. Осталась еще куча вариантов для размещения других букв (например, русских).
Остальным клавишам тоже нашлось место на клавиатуре. У меня получилась такая раскладка:

Использовался редактор нотной записи MuseScore.
На видео, приложенном к посту, продемонстрирован набор программы Hello world и ее компиляция с использованием синтезатора вместо клавиатуры.
Код программы выложен на Github.
Чтение данных из синтезатора
Итак, в наличии имеется ноутбук с 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.
