В этой статье я хочу рассказать про парсинг MIDI сообщений из midi файла. В сети можно найти несколько библиотек,
которые читают midi файлы и посылают команды на определенный midi порт, но если Вам понадобиться работать с midi файлами созданными в таких программах как Cubase или FruityLoops, то они(библиотеки) могут Вам не подойти, так как эти программы хоть и придерживаются стандартов midi файлов, различия в сохранинии midi файлов могут быть значительными.
Примеры написаны на языке c++.
Нам необходимо выделить из миди файла: имя инструмента и соответствующие ему ноты.
1) Находим начало информационного блока одного инструмента:
Указателем на то, что в миди файле начинается блок с командами служит последовательность из 4-х байтов 'MRtk'.
В этом блоке содержатся имя инструмента и последовательность команд.
В такой программе, как Cubase, даже если в midi файле содержится один инструмент, в нем все равно содержится две записи MRtk, поэтому первую запись нам необходимо пропустить. (в первой записи содержится информация не относящаяся к какому-либо инструменту, что соответственно противоречит стандарту миди файлов, но возможно Вам будет необходима какая-либо информация из него)
Смысл в том, что бы пройти весь массив с командами, пока не встретим нужную нам запись MRtk.
которые читают midi файлы и посылают команды на определенный midi порт, но если Вам понадобиться работать с midi файлами созданными в таких программах как Cubase или FruityLoops, то они(библиотеки) могут Вам не подойти, так как эти программы хоть и придерживаются стандартов midi файлов, различия в сохранинии midi файлов могут быть значительными.
Примеры написаны на языке c++.
Нам необходимо выделить из миди файла: имя инструмента и соответствующие ему ноты.
1) Находим начало информационного блока одного инструмента:
Указателем на то, что в миди файле начинается блок с командами служит последовательность из 4-х байтов 'MRtk'.
В этом блоке содержатся имя инструмента и последовательность команд.
В такой программе, как Cubase, даже если в midi файле содержится один инструмент, в нем все равно содержится две записи MRtk, поэтому первую запись нам необходимо пропустить. (в первой записи содержится информация не относящаяся к какому-либо инструменту, что соответственно противоречит стандарту миди файлов, но возможно Вам будет необходима какая-либо информация из него)
Смысл в том, что бы пройти весь массив с командами, пока не встретим нужную нам запись MRtk.
char * track; // указатель на начало массива, в котором загружены все комманды из midi файла.
int buff_point; // переменная, которая увеличивается в цикле.
if (*(track+buff_point) == 'M' && *(track+buff_point+1) == 'T' && *(track+buff_point+2) == 'r' && *(track+buff_point+3) == 'k'){
buff_point+=4; // Если мы нашли запись MRtk, нам необходимо переместиться через неё и дальше действовать по другому
}
* This source code was highlighted with Source Code Highlighter.
2) После того как мы нашли начало записи инструмента нам необходимо выяснить название этого инструмента. Для этого нам необходимо найти последовательность байт FF 03. В десятичной форме соответственно 255 03
Сразу за этой последовательностью байт следует размер (в байтах) имени инструмента. Соответственно мы узнаем длинну имени инструмента и копируем нужное количество байт в буффер имени инструмента.
if ((unsigned char)*(track+buff_point) == 255 && (unsigned char)*(track+buff_point+1) == 3){
buff_point+=2;
int name_lenght=(unsigned char)*(track+buff_point);
inst_name=new char[name_lenght+1];
ZeroMemory(inst_name, name_lenght+1);
buff_point++;
for (int i=0; i < name_lenght; i++){
*(inst_name+i)=(unsigned char)*(track+buff_point);
buff_point++;
}
continue;
}
* This source code was highlighted with Source Code Highlighter.
Это можно было сделать и функцией memcpy(), но в данном случае у меня все завязано на переменной buff_point, которая увеличивается в цикле, и для наглядности я копирую таким способом.
3) Сразу за байтами имени инструмента следует последовательность комманд «взять ноту» и «отпустить ноту».
Взятие ноты — значение байта от 90 до 9F.
Отпускание ноты — значение байта от 80 до 8F.
Значения от 0 до F соответствуют номеру канала, на котором следует брать или отпускать ноту.
Перед номером команды идет дельта-времени от предыдущей команды, после — номер ноты (от 0 до 127), за ним — длительность нажатия/отпускания.
Полностью первая команда может выглядеть так: 00 90 54 64
Дельта-времени это величина в тиках от предыдущей команды. Проблема в том, что она может состоять как из одного байта, так и из 2-х, 3-х, или 4-х байт.
Для того, что бы было понятно, что кончились байты дельта-времени и начался байт команды, в крайнем (правом) байте старший бит всегда равен '0', и мы можем выделить этим конец байтов дельта-времени,
т.к. у следующего байта старший бит будет равен 1 (команды 90 или 80).
Пример: (начало байтов дельта-времени)->10010111 10010110 01101001<-(конец байтов дельта-времени) (команда 90)->10010000
Здесь мы видим, что блок дельта-времени состоит из 3-х байтов, и сигналом на то, что третий байт последний служит нулевой старший бит у этого байта и не нулевой старший бит у следующего байта.
Согласно стандарту информационными являются только 7 правых битов у каждого байта дельта-времени.
Если у нас несколько таких байтов, нам необходимо учесть только нужные нам биты из них.
Далее код:
unsigned char delta_time_source[4];
ZeroMemory(delta_time_source, 4);
unsigned char * p_delta_time_source=delta_time_source;
int delta_time_bit=1;
int delta_time_source_bit=0;
int delta_time=0;
int * p_delta_time=&delta_time;
// ищем байт, в котором заканчиваются байты дельта-времени и начинаются байты миди команды
int i=0;
do {
i++;
} while (!(~((unsigned char)*(track+buff_point+i-1) | 127) & 128) || !((unsigned char)*(track+buff_point+i) & 128));
int n=buff_point+i;
buff_point+=i-1;
// изначально дельта времени - массив из четырех элементов, который заполняется начиная с 4-го значениями байтов из дельта времени
int f=3;
while (f >= 0){
*(delta_time_source+f)=(unsigned char)*(track+buff_point);
f--;
i--;
buff_point--;
if (i==0) break; // мы копируем в него только i байтов
}
//возвращаем позицию на значение команды
buff_point=n;
// далее из каждого элемента массива мы рассматриваем только 7 бит, и если рассматриваемый бит равен "1" - ставим в конечном значении дельта-времени(delta_time) "1".
int counter=0;
for (int i=3; i>=0; i--){
for (int j=0; j<7; j++){
if (*(delta_time_source+i) & ((char)1 << j)){
delta_time_source_bit=(char)1 << counter;
} else {
delta_time_source_bit=0;
}
delta_time=delta_time ^ delta_time_source_bit;
counter++;
}
}
int cmd=(unsigned char)*(track+buff_point);
buff_point++;
int note=(unsigned char)*(track+buff_point);
buff_point++;
int velo=(unsigned char)*(track+buff_point);
buff_point++;
* This source code was highlighted with Source Code Highlighter.
Мы получили значение дельта времени, номер ноты и скорость нажимания/отпускания для одной команды. В цикле можно получить эти значения для других нот.
Длительность тика дельта-времени можно узнать из 2-х байтов начиная с 13-го от начала midi файла — в этой записи содержится количество тиков в одной четверти.
Понять, что дошли до конца записи MRtk можно выделив 3 байта FF 2F 00.