
Имеем:
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 х2 быстрое сильное нажатие,
С1 х1 cильное долгое нажатие
D1 х1 быстрое нажатие
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().
Собственно завершив "колдовать" получаем готовый результат и возможность насладиться игрой на инструменте.
P.S. С помощью ALSA уже позже получилось прикрутить клавиатуру к LMMS и Reaper, просто не сразу дошли некоторые вещи.
