Неистовые потуги или как поиграть на midi-клавиатуре в стиле linux-way

от автора

Имеем:

  • M-AUDIO KeyStation 61

  • Ubuntu 20.04.3 LTS x86_64

  • Умение в Java

  • Вечное желание замутить что-то собственными руками

  • Не преодолимое желание поиграть свои любимые мелодии

При использовании KeyStation c LMMS или любым другим синтезирующим звук софтом в системе Windows, проблем не возникает, там всё автоматически подключается без необходимости дополнительных действий и настроек. Но поскольку я являюсь фанатом Linux, то по максимуму стараюсь вертеться исключительно в приделах OpenSource / Freeware программного обеспечения. По этому Windows-way когда всё начинает работать с минимальными усилиями это не про меня.

Собственно небольшая предыстория

Сначала я пытался подключить midi-клавиатуру с помощью родных средств системы, но результата добиться не получилось. Потом по относительно древним инструкциям устанавливал пакеты Ubuntu-Studio, но мои отношения с аудиоподсистемой Jack не сложились. Но в моментах попытки всё настроить периодически поглядывал на то что устройство всё-таки определяется с помощью утилиты lsusb и наличии устройства в директории /dev в которой мой миди-девайс определялся как dmmidi1 ( Естественно что это именно она, определял методом втыка-вытыка USB провода в хаб ). Из большого любопытства я открыл этот файлик с помощью cat, и обнаружил что при взаимодействии с девайсом, в этот файлике пролетают байтики. Тут у меня и проскочила мысль, что если у меня не складываются отношения с «родными» аудио подсистемами линуха, то стоит попробовать провзаимодействовать с клавиатурой напрямую. Да бы разобраться с пролетающими там последовательностями накидал простейший код чтения потока данных из файла —

try {   var fis = new FileInputStream("/dev/" + MIDI_DEVICE_NAME);    int data = 0;   int counter = 0;    boolean playOn;    while(data != -1) {     data = fis.read();     System.out.printf("--- %d --- %s --- ", ++counter, (char) data);     System.out.println(data);   }    fis.close(); } catch (IOException io){   System.out.println(io.getLocalizedMessage()); }

Произвел следующие нажатия по клавиатуре —

  1. С1 х2 быстрое сильное нажатие,

  2. С1 х1 cильное долгое нажатие

  3. D1 х1 быстрое нажатие

  4. E1 x1 быстрое нажатие

Где первая буква это нота, а рядом стоящая цифра это октава ( относительно расположения на клавиатуре) ( Для получения более точных результатов громкость на клавиатуре стоит на максимум, отклонение ( хоть как потом выяснилось это не влияет )

По итогу в аутпуте терминала я получил следующее —

Получившийся лог
Counter  Char   Byte  --- 1 --- $ --- 36 --- 2 --- j --- 106 --- 3 --- ’ --- 146 --- 4 --- $ --- 36 --- 5 ---   --- 0  --- 6 --- ’ --- 146 --- 7 --- $ --- 36 --- 8 --- g --- 103 --- 9 --- ’ --- 146 --- 10 --- $ --- 36 --- 11 ---   --- 0  --- 12 --- ’ --- 146 --- 13 --- $ --- 36 --- 14 --- L --- 76 --- 15 --- ’ --- 146 --- 16 --- $ --- 36 --- 17 ---   --- 0  --- 18 --- ’ --- 146 --- 19 --- & --- 38 --- 20 --- Z --- 90 --- 21 --- ’ --- 146 --- 22 --- & --- 38 --- 23 ---   --- 0  --- 24 --- ’ --- 146 --- 25 --- ( --- 40 --- 26 --- b --- 98 --- 27 --- ’ --- 146 --- 28 --- ( --- 40 --- 29 ---   --- 0

( Логи специально разбил пустыми строками для удобности чтения )

Зная какие клавиши и как нажимались, можно догадаться что — за одно нажатие (имеется виду полное совершённое действие — клавиша зажата и в дальнейшем отпущена ) мы в среднем получаем по 6-байт полезной информации, от этого и будем отталкиваться. (в логе я уже вручную специально разбил информацию по 6 строк да бы было проще читать) — Значение 112 как я понял — является чем-то типа заголовка канала, поэтому будем отталкиваться от него — Следующим байтом мы видим значения 36, 38, 40, что вполне логично, так как между нотами С и D, D и E есть ещё и полутона С# и D# соответственно ( Черный клавиши на клавиатуре ) — Третьим же байтом мы получаем силу удара по клавише, иначе же — громкость с которой нужно воспроизводить ноту от 1 до 127, и 0 когда клавиша отпущена.

Первый байт

Второй байт

Третий байт

97-119 идентификатор канала

24-108 идентификатор ноты

1-127 — громкость (velocity)

0 — когда кнопка отпущена

Зная что в поставке с Java есть неплохой инструментарий для работы с midi возвращаемся к магии «прогрузирования«. —

Код получившейся программы
package dev.xred.ServlessMidiSynth;  import java.io.*; import java.util.HashMap;  public class ServerLessMidiSynthesizer {      private static final String MIDI_DEVICE_NAME = "dmmidi1";      static HashMap<Byte, Thread> playing = new HashMap<>();      public static void main(String[] args) throws IOException {          SimpleLogger logger = new SimpleLogger(ServerLessMidiSynthesizer.class);          try(var fis = new FileInputStream("/dev/" + MIDI_DEVICE_NAME); var buf = new BufferedInputStream(fis)){             byte[] in = buf.readNBytes(3);              while(loop) {                 NotePlayer notePlayer = null;                 Thread thread = null;                 logger.print((in[2] != 0 ? "pull in " : "pull out") + in[0] + " " + in[1] + " " + in[2]);              // Cмотрим является ли новый набор байт командой играть ноту или же терминальной командой                 if(in[2] != 0 ){                     // Играем ноту                     notePlayer = new NotePlayer(in[1], in[2]);                     thread = new Thread(notePlayer);                     thread.start();                     playing.put(in[1], thread);                 } else {                     //Прекращаем воспроизведение ноты                     thread = playing.get(in[1]);                      if(thread != null){                         thread.interrupt();                         thread.stop();                          playing.remove(in[1]);                     }                  }                 in = buf.readNBytes(3);             }          } catch (IOException io){             logger.print(io.getLocalizedMessage());         }     } }
package dev.xred.ServlessMidiSynth;  import javax.sound.midi.*;  public class NotePlayer implements Runnable {      private int note;     private int velocity;      public boolean playOn;      public NotePlayer(int note, int velocity){         this.note = note;         this.velocity = velocity;     }      public int getNote() {         return note;     }      public int getVelocity() {         return velocity;     }      @Override     public void run()          var midiPlayer = MidiPlayer.getInstance();         var mc = midiPlayer.getMidiChannel();          velocity += 50;         mc.noteOn(note, velocity);        // Стоит тут не просто так, нужен для того чтобы -       // выдержать длинну играемой ноты         while(!Thread.interrupted());          mc.noteOff(note);     } } 
package dev.xred.ServlessMidiSynth;  import javax.sound.midi.*;  public class MidiPlayer {      private int lastOutputed = 0;      private static MidiPlayer instance;      private MidiChannel[] mChannels;     private Instrument[] instr;   // Полный список инструментов можно получить пройдясь по массиву instr     private int instrumentNum = 0;          private MidiPlayer(){         Synthesizer midiSynth = null;          try {             midiSynth = MidiSystem.getSynthesizer();             midiSynth.open();         } catch (MidiUnavailableException e) {             e.printStackTrace();         }          instr = midiSynth.getDefaultSoundbank().getInstruments();         mChannels = midiSynth.getChannels();                if(midiSynth.loadInstrument(instr[instrumentNum])) System.out.println("instrument - " + instrumentNum + "has been loaded");;     }      public static MidiPlayer getInstance() {         if(instance == null)             instance = new MidiPlayer();          return instance;     }      public MidiChannel getMidiChannel(){       // Не большой костыль - пришлось раскидать звук на несколько каналов, т.к.       // при тестах(игре) почему-то происходило переполнение канала       // из-за чего вылетало исключение         if (lastOutputed > 3) lastOutputed = 0;         return mChannels[lastOutputed++];     }  } 

FileInputStream решил использовать только как промежуточную стадию, и грузить все через BufferedStream. Когда мы получили доступ к «буферному читале» забираем сразу по 3 байта которые нам так необходимы. Если забирать по одному, то кода для обработки будет намного больше и работать будет намного медленнее, чего нам по максимум нужно избегать. В плане обработчика начала и конца воспроизведения ноты сделал отдельный класс NotePlayer реализующий Runnable, куда скидываю в конструктор параметры т.е. 2 и 3 байт. Запихиваю всё в новый поток, а уже поток в HashMap для хранения и слежения за ним, и параллельно с чтением смотрю, проигрывается ли нота, и если да, при нуле в 3-ем байте вызываю interupt().


Собственно завершив «колдовать» получаем готовый результат и возможность насладиться игрой на инструменте.

Ссылка на Github-репозиторий

P.S. С помощью ALSA уже позже получилось прикрутить клавиатуру к LMMS и Reaper, просто не сразу дошли некоторые вещи.


ссылка на оригинал статьи https://habr.com/ru/articles/583624/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *