Furious attempts or how to play midi-keyboard in linux-way style

We have:

  • M-AUDIO KeyStation 61

  • Ubuntu 20.04.3 LTS x86_64

  • Skill in Java

  • The eternal desire to stir up something with your own hands

  • Irresistible urge to play your favorite tunes

When using KeyStation c LMMS or any other software synthesizing sound in system Windows, there are no problems, everything is automatically connected there without the need for additional actions and settings. But since I am a fan Linux, then to the maximum I try to rotate exclusively in the aisles OpenSource / Freeware software. That’s why Windows-way when everything starts working with minimal effort it’s not about me.

Actually a little background

At first I tried to connect a midi keyboard using the native tools of the system, but the result was not achieved. Then, according to relatively ancient instructions, I installed the packages Ubuntu-Studiobut my relationship with the audio subsystem Jack did not work out. But in moments of trying to set everything up, I periodically glanced at the fact that the device is still detected using the utility lsusb and the presence of the device in the directory / dev in which my midi device was defined as dmmidi1 ( Naturally, it was she who determined the method of plugging-in-plugging the USB cable into the hub). Out of curiosity, I opened this file with cat, and found that when interacting with the device, bytes fly by in this file. Then I got the idea that if I don’t have a good relationship with the “native” audio subsystems of the linux, then I should try to interact with the keyboard directly. Yes, to deal with the sequences flying there, I threw the simplest code for reading a data stream from a file –

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());
}

Made the following keystrokes on the keyboard –

  1. C1 x2 quick strong press,

  2. C1 x1 hard long press

  3. D1 x1 quick press

  4. E1 x1 quick press

Where the first letter is a note, and the adjacent number is an octave ( relative to keyboard location) ( To get more accurate results, the volume on the keyboard is set to maximum, deviation (although as it turned out later, this does not affect)

As a result, in output terminal, I got the following –

The resulting log
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

(Logs were deliberately broken with empty lines for readability)

Knowing which keys and how they were pressed, you can guess what – in one press (I mean the complete performed action – the key is held down and then released) we get on average 6-byte useful information, and we will build on this. (in the log, I already manually split the information into 6 lines so it would be easier to read) – Meaning 112 as I understand it – it is something like a channel header, so we will build on it – The next byte we see the values 36, 38, 40, which is quite logical, since between the notes WITH and D, D and E there are also semitones WITH# and D # respectively ( Black keys on keyboard) – With the third byte we get the strength of the strike on the key, otherwise – the volume with which you need to play the note from 1 before 127, and 0 when the key is released.

First byte

Second byte

Third byte

97-119 channel ID

24-108 note ID

1-127 – volume (velocity)

0 – when the button is released

Knowing that Java comes with a good toolkit for working with midi, let’s get back to magic “loading“. –

The resulting program code
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++];
    }

}

I decided to use FileInputStream only as an intermediate stage, and load everything through BufferedStream. When we got access to “buffered reader“we pick up immediately on 3 bytes which we need so much. If we take one at a time, then the code for processing will be much larger and will work much slower, which we should avoid as much as possible. In terms of the handler for the beginning and end of playing a note, I made a separate class NotePlayer realizing Runnable, where I drop the parameters into the constructor i.e. 2 and 3 byte. I stuff everything into a new stream, and already the stream into HashMap for storing and tracking it, and in parallel with reading I see if a note is being played, and if so, when zero v 3rd byte calling interupt ()


Having actually completed the “conjure”, we get the finished result and the opportunity to enjoy playing the instrument.

Link to Github repository

PS With the help of ALSA, it turned out later to screw the keyboard to LMMS and Reaper, just some things did not reach right away.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *