Имеем:
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)
Tiriet
15.10.2021 12:22+4А у меня противоположная ситуация. MAudio ES-88, под виндой не смог побороть лаг в 100мс- вроде мелочь, но при игре- очень сильно ощущается и играть невозможно. А в Убунту-Студио все завелось с полпинка через Jack и работает уже два года на стареньком атоме (матплата от нетбука в деревянном корпусе с навешенным сверху китайским девятидюймовым тачскрином).
ris58h
15.10.2021 12:59+3По треду на ноту - это мощно.
redfraction Автор
15.10.2021 15:54Cогласен криво, но в уставшую голову после рабочего дня почему-то только это пришло)
piratarusso
15.10.2021 14:44+1Я в разное время подключал разные миди клавы(Korg/Roland). В Linux никаких проблем с этим не встречал. Сама клавиатура обычно подхватывается через alsa, если нужно JACK, то jackctl умеет подключать их через alsa. Правильно написано тут - USB MIDI keyboards - ArchWiki (archlinux.org)
astenix
15.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.