
Имеем:
- 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, просто не сразу дошли некоторые вещи.
Комментарии (7)
 - Tiriet15.10.2021 12:22+4- А у меня противоположная ситуация. MAudio ES-88, под виндой не смог побороть лаг в 100мс- вроде мелочь, но при игре- очень сильно ощущается и играть невозможно. А в Убунту-Студио все завелось с полпинка через Jack и работает уже два года на стареньком атоме (матплата от нетбука в деревянном корпусе с навешенным сверху китайским девятидюймовым тачскрином). 
 - ris58h15.10.2021 12:59+3- По треду на ноту - это мощно.  - redfraction Автор15.10.2021 15:54- Cогласен криво, но в уставшую голову после рабочего дня почему-то только это пришло) 
 
 - piratarusso15.10.2021 14:44+1- Я в разное время подключал разные миди клавы(Korg/Roland). В Linux никаких проблем с этим не встречал. Сама клавиатура обычно подхватывается через alsa, если нужно JACK, то jackctl умеет подключать их через alsa. Правильно написано тут - USB MIDI keyboards - ArchWiki (archlinux.org) 
 - astenix15.10.2021 17:01- Буквально вчера решал почти такую же задачу, но с другим expected result. Клавиатура нужна только периодически, чтобы подобрать мелодию (на гитаре с этим бывает сложно), и для одной только правой руки несколько октав достаточно. - Есть большая пианина «Yamaha psr-E453», там быстрый вкл/выкл, встроенный звук, но она БОЛЬШАЯ (61 клавиша), на столе ей места нет. Поэтому взял миди-клаву «Midiplus X2 mini» (25 клавиш), по размерам она устраивается перед ноутом в самый раз. - Под виндой в LMMS она воткнулась и работает. В дебе в LMMS вроде бы воткнулась, но дальше надо в jack правильно протащить связи, и тут я не осилил, было несколько подходов с серией экспериментов, но никак. Да и сценарий использования получается многоступенчатым, обязательно ноут включить, в нем софт… Так и отложил. - Поискал синтезаторы малого размера, и нашел «Yamaha pss-A50» (37 клавиш). Работает или от батареек, или от usb, и может работать как миди-клавиатура, но в моем сценарии это "на потом". Ценно то, что в ней встроенный динамик (один; можно и в наушники), быстрое вкл/выкл, есть возможность отключить динамику нажатия клавиш (она там препаршивая, по клавиша нужно аж стучать), можно двигать октавы вверх-вниз двумя кнопками. Реально спасло! 
 
           
 
Javian
Было бы не плохо добавить опцию записи в midi файл. Например, моё цифровое пианино софт для работы с ПК имеет только для Windows и оно практически бесполезное.
redfraction Автор
Если проект окажется хоть сколько-то востребован может и задумаюсь над каким-то доп функционалом, и возможно разработкой GUI.