Разместить здесь вашу рекламу


Неистовые потуги или как поиграть на 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/post/583624/


Интересные статьи

Интересные статьи

SWAP (своп) — это механизм виртуальной памяти, при котором часть данных из оперативной памяти (ОЗУ) перемещается на хранение на HDD (жёсткий диск), SSD (твёрдотельный накоп...
В 1С-Битрикс: Управление сайтом (как и в Битрикс24) десятки, если не сотни настраиваемых типов данных (или сущностей): инфоблоки, пользователи, заказы, склады, форумы, блоги и т.д. Стр...
Есть статьи о недостатках Битрикса, которые написаны программистами. Недостатки, описанные в них рядовому пользователю безразличны, ведь он не собирается ничего программировать.
Эта статья для тех, кто собирается открыть интернет-магазин, но еще рассматривает варианты и думает по какому пути пойти, заказать разработку магазина в студии, у фрилансера или выбрать облачный серви...
В статье описаны необходимые параметры сервера для оптимальной работы сайта на платформе 1С-Битрикс.