Недавно мне в голову пришла мысль: а нельзя ли, подключив синтезатор к компьютеру, набирать на нем текст? Я попробовал реализовать это, и у меня получилось. Моя программа считывает нажатия клавиш синтезатора и эмулирует нажатия клавиш обычной клавиатуры. В этой статье я расскажу, как это реализовать. Писать будем под 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.
Автор: Riateche