exponenta event banner

Преобразование MIDI-файлов в MIDI-сообщения

В этом примере показано, как преобразовать обычные MIDI-файлы в представление сообщений MIDI с помощью Audio Toolbox™. В этом примере выполняется следующее:

  1. Чтение двоичного MIDI-файла в рабочую область MATLAB ®.

  2. Преобразование данных MIDI-файла в midimsg объекты.

  3. Воспроизводите MIDI-сообщения на звуковой карте с помощью простого синтезатора.

Дополнительные сведения о взаимодействии с устройствами MIDI с помощью MATLAB см. в разделе Интерфейс устройств MIDI. Для получения дополнительной информации о MIDI в целом обратитесь в Ассоциацию производителей MIDI.

Введение

MIDI-файлы содержат MIDI-сообщения, информацию о времени и метаданные о кодированной музыке. В этом примере показано, как извлекать MIDI-сообщения и информацию о синхронизации. Чтобы упростить код, в этом примере метаданные игнорируются. Поскольку метаданные включают в себя такие сведения, как временная сигнатура и темп, в этом примере предполагается, что MIDI-файл находится в 4/4 времени со скоростью 120 ударов в минуту (BPM).

Прочитать MIDI-файл

Чтение MIDI-файла с помощью fread функция. fread функция возвращает вектор байтов, представленный целыми числами.

readme = fopen('CmajorScale.mid');
[readOut, byteCount] = fread(readme);
fclose(readme);

Преобразовать данные MIDI в midimsg Объекты

MIDI-файлы содержат блоки заголовков и блоков отслеживания. Блоки заголовков предоставляют основную информацию, необходимую для интерпретации остальной части файла. Файлы MIDI всегда начинаются с фрагмента заголовка. Блоки дорожки следуют за блоком заголовка. Блоки отслеживания предоставляют MIDI-сообщения, информацию синхронизации и метаданные файла. Каждый блок дорожки имеет заголовок блока дорожки, который включает в себя длину блока дорожки. Блок дорожек содержит события MIDI после заголовка блока дорожек. Каждое событие MIDI имеет дельта-время и сообщение MIDI.

Разбор фрагмента заголовка MIDI

Блок заголовка MIDI включает в себя разделение файла по времени. Разделение по времени определяет, как интерпретировать разрешение засечек в MIDI-файле. Галочки - это единица времени, используемая для установки меток времени для MIDI-файлов. MIDI-файл с большим количеством засечек в единицу времени содержит MIDI-сообщения с более детализированными отметками времени. Разделение по времени не определяет темп. MIDI-файлы определяют разделение по времени либо по галочкам в квартальной заметке, либо по кадрам в секунду. В этом примере предполагается, что разделение по времени MIDI находится в клещах в квартальной заметке.

fread функция считывает двоичные файлы по байтам, но разделение по времени сохраняется как 16-разрядное (2-байтовое) значение. Чтобы вычислить несколько байтов как одно значение, используйте polyval функция. Вектор байтов может быть оценен как многочлен, где x установлен на 256. Например, вектор байтов [1 2 3] может оцениваться как:

1•2562+2•2561+3•2560

% Concatenate ticksPerQNote from 2 bytes
ticksPerQNote = polyval(readOut(13:14),256);

Синтаксический анализ блока дорожек MIDI

Блок дорожек MIDI содержит события заголовка и MIDI. Заголовок блока дорожки содержит длину блока дорожки. Остальная часть блока дорожек содержит одно или несколько событий MIDI.

Все события MIDI имеют два основных компонента:

  • Значение дельта-времени - разница во времени между предыдущим событием MIDI-трека и текущим событием

  • Сообщение MIDI - необработанные данные события MIDI track

Чтобы последовательно анализировать события MIDI-отслеживания, создайте цикл в цикле. Во внешнем цикле разбор частей дорожки, итерация chunkIndex. Во внутреннем цикле анализируйте события MIDI, итерируя указателем ptr.

Чтобы проанализировать события MIDI-отслеживания:

  • Считывание значения дельта-времени по указателю.

  • Увеличьте указатель на начало MIDI-сообщения.

  • Прочитайте MIDI-сообщение и извлеките соответствующие данные.

  • Добавьте MIDI-сообщение в массив MIDI-сообщений.

По завершении просмотрите массив сообщений MIDI.

% Initialize values
chunkIndex = 14;     % Header chunk is always 14 bytes
ts = 0;              % Timestamp - Starts at zero
BPM = 120;                  
msgArray = [];              

% Parse track chunks in outer loop
while chunkIndex < byteCount
    
    % Read header of track chunk, find chunk length   
    % Add 8 to chunk length to account for track chunk header length
    chunkLength = polyval(readOut(chunkIndex+(5:8)),256)+8;
    
    ptr = 8+chunkIndex;             % Determine start for MIDI event parsing
    statusByte = -1;                % Initialize statusByte. Used for running status support
    
    % Parse MIDI track events in inner loop
    while ptr < chunkIndex+chunkLength
        % Read delta-time
        [deltaTime,deltaLen] = findVariableLength(ptr,readOut);  
        % Push pointer to beginning of MIDI message
        ptr = ptr+deltaLen;
        
        % Read MIDI message
        [statusByte,messageLen,message] = interpretMessage(statusByte,ptr,readOut);
        % Extract relevant data - Create midimsg object
        [ts,msg] = createMessage(message,ts,deltaTime,ticksPerQNote,BPM);
        
        % Add midimsg to msgArray
        msgArray = [msgArray;msg];
        % Push pointer to next MIDI message
        ptr = ptr+messageLen;
    end
    
    % Push chunkIndex to next track chunk
    chunkIndex = chunkIndex+chunkLength;
end
disp(msgArray)
  MIDI message:
    NoteOn          Channel: 1  Note: 60  Velocity: 127 Timestamp: 0  [ 90 3C 7F ]
    NoteOff         Channel: 1  Note: 60  Velocity: 0   Timestamp: 0.5  [ 80 3C 00 ]
    NoteOn          Channel: 1  Note: 62  Velocity: 127 Timestamp: 0.5  [ 90 3E 7F ]
    NoteOff         Channel: 1  Note: 62  Velocity: 0   Timestamp: 1  [ 80 3E 00 ]
    NoteOn          Channel: 1  Note: 64  Velocity: 127 Timestamp: 1  [ 90 40 7F ]
    NoteOff         Channel: 1  Note: 64  Velocity: 0   Timestamp: 1.5  [ 80 40 00 ]
    NoteOn          Channel: 1  Note: 65  Velocity: 127 Timestamp: 1.5  [ 90 41 7F ]
    NoteOff         Channel: 1  Note: 65  Velocity: 0   Timestamp: 1.75  [ 80 41 00 ]
    NoteOn          Channel: 1  Note: 67  Velocity: 127 Timestamp: 2  [ 90 43 7F ]
    NoteOff         Channel: 1  Note: 67  Velocity: 0   Timestamp: 2.5  [ 80 43 00 ]
    NoteOn          Channel: 1  Note: 69  Velocity: 127 Timestamp: 2.5  [ 90 45 7F ]
    NoteOff         Channel: 1  Note: 69  Velocity: 0   Timestamp: 3  [ 80 45 00 ]
    NoteOn          Channel: 1  Note: 71  Velocity: 127 Timestamp: 3  [ 90 47 7F ]
    NoteOff         Channel: 1  Note: 71  Velocity: 0   Timestamp: 3.5  [ 80 47 00 ]
    NoteOn          Channel: 1  Note: 72  Velocity: 127 Timestamp: 3.5  [ 90 48 7F ]
    NoteOff         Channel: 1  Note: 72  Velocity: 0   Timestamp: 3.75  [ 80 48 00 ]
    NoteOn          Channel: 1  Note: 72  Velocity: 127 Timestamp: 4  [ 90 48 7F ]
    NoteOff         Channel: 1  Note: 72  Velocity: 0   Timestamp: 4.5  [ 80 48 00 ]
    NoteOn          Channel: 1  Note: 71  Velocity: 127 Timestamp: 4.5  [ 90 47 7F ]
    NoteOff         Channel: 1  Note: 71  Velocity: 0   Timestamp: 5  [ 80 47 00 ]
    NoteOn          Channel: 1  Note: 69  Velocity: 127 Timestamp: 5  [ 90 45 7F ]
    NoteOff         Channel: 1  Note: 69  Velocity: 0   Timestamp: 5.5  [ 80 45 00 ]
    NoteOn          Channel: 1  Note: 67  Velocity: 127 Timestamp: 5.5  [ 90 43 7F ]
    NoteOff         Channel: 1  Note: 67  Velocity: 0   Timestamp: 5.75  [ 80 43 00 ]
    NoteOn          Channel: 1  Note: 65  Velocity: 127 Timestamp: 6  [ 90 41 7F ]
    NoteOff         Channel: 1  Note: 65  Velocity: 0   Timestamp: 6.5  [ 80 41 00 ]
    NoteOn          Channel: 1  Note: 64  Velocity: 127 Timestamp: 6.5  [ 90 40 7F ]
    NoteOff         Channel: 1  Note: 64  Velocity: 0   Timestamp: 7  [ 80 40 00 ]
    NoteOn          Channel: 1  Note: 62  Velocity: 127 Timestamp: 7  [ 90 3E 7F ]
    NoteOff         Channel: 1  Note: 62  Velocity: 0   Timestamp: 7.5  [ 80 3E 00 ]
    NoteOn          Channel: 1  Note: 60  Velocity: 127 Timestamp: 7.5  [ 90 3C 7F ]
    NoteOff         Channel: 1  Note: 60  Velocity: 0   Timestamp: 7.75  [ 80 3C 00 ]
    AllNotesOff     Channel: 1  Timestamp: 8  [ B0 7B 00 ]

Синтезировать MIDI-сообщения

В этом примере воспроизводятся проанализированные MIDI-сообщения с помощью простого монофонического синтезатора. Демонстрацию этого синтезатора см. в разделе Проектирование и воспроизведение синтезатора MIDI.

% Initialize System objects for playing MIDI messages
osc = audioOscillator('square', 'Amplitude', 0,'DutyCycle',0.75);
deviceWriter = audioDeviceWriter;

simplesynth(msgArray,osc,deviceWriter);

Вы также можете отправлять проанализированные MIDI-сообщения на устройство MIDI с помощью midisend. Дополнительные сведения о взаимодействии с устройствами MIDI с помощью MATLAB см. в разделе Интерфейс устройств MIDI.

Вспомогательные функции

Считывание дельта-таймов

Дельта-времена событий MIDI-отслеживания сохраняются как значения переменной длины. Эти значения имеют длину от 1 до 4 байт, причем в качестве флага используется старший бит каждого байта. Самый значащий бит последнего байта устанавливается в 0, а самый значащий бит каждого другого байта устанавливается в 1.

В событии MIDI-трека дельта-время всегда размещается перед сообщением MIDI. Промежуток между дельта-временем и концом предыдущего события MIDI отсутствует.

findVariableLength функция считывает значения переменной длины, например дельта-раз. Возвращает длину входного значения и само значение. Сначала функция создает 4-байтовый вектор byteStream, для которого установлены все нули. Затем он нажимает указатель на начало события MIDI. Функция проверяет четыре байта после указателя в цикле. Для каждого байта проверяется старший бит (MSB). Если MSB равен нулю, findVariableLength добавляет байт в byteStream и выходит из цикла. В противном случае он добавляет байт к byteStream и переходит к следующему байту.

Один раз findVariableLength функция достигает конечного байта значения переменной длины, она оценивает байты, собранные в byteStream с использованием polyval функция.

function [valueOut,byteLength] = findVariableLength(lengthIndex,readOut)

byteStream = zeros(4,1);

for i = 1:4
    valCheck = readOut(lengthIndex+i);
    byteStream(i) = bitand(valCheck,127);   % Mask MSB for value
    if ~bitand(valCheck,uint32(128))        % If MSB is 0, no need to append further
        break
    end
end

valueOut = polyval(byteStream(1:i),128);    % Base is 128 because 7 bits are used for value
byteLength = i;

end

Интерпретировать MIDI-сообщения

В MIDI-файлах существует три основных типа сообщений:

  • Сообщения Sysex - сообщения, исключительные для системы, игнорируются в этом примере.

  • Мета-события - могут происходить вместо MIDI-сообщений для предоставления метаданных для MIDI-файлов, включая название песни и темп. midimsg не поддерживает мета-события. В этом примере мета-события игнорируются.

  • MIDI-сообщения - анализируется в этом примере.

Чтобы интерпретировать MIDI-сообщение, прочитайте байт состояния. Байт состояния является первым байтом сообщения MIDI.

Несмотря на то, что в этом примере игнорируются сообщения Sysex и мета-события, важно идентифицировать эти сообщения и определить их длину. Длины сообщений Sysex и мета-событий являются ключевыми для определения места начала следующего сообщения. Сообщения Sysex имеют 'F0' или 'F7' как байт состояния, так и мета-события 'FF' в качестве байта состояния. Сообщения Sysex и мета-события могут иметь различную длину. После байта состояния сообщения Sysex и мета-события указывают длину события. Значения длины события являются значениями переменной длины, такими как значения дельта-времени. Продолжительность события может быть определена с помощью findVariableLength функция.

Для сообщений MIDI длина сообщения может определяться значением байта состояния. Однако MIDI-файлы поддерживают рабочее состояние. Если MIDI-сообщение имеет тот же байт состояния, что и предыдущее MIDI-сообщение, байт состояния может быть опущен. Если первый байт входящего сообщения не является допустимым байтом состояния, используйте байт состояния предыдущего MIDI-сообщения.

interpretMessage функция возвращает байт состояния, длину и вектор байтов. Байт состояния возвращается во внутренний цикл в случае, если следующее сообщение является сообщением о состоянии выполнения. Длина возвращается во внутренний цикл, где указывается, как далеко следует толкать указатель внутреннего цикла. Наконец, вектор байтов несет необработанные двоичные данные MIDI-сообщения. interpretMessage требует вывода, даже если функция игнорирует данное сообщение. Для сообщений Sysex и мета-событий, interpretMessage прибыль -1 вместо вектора байтов.

function [statusOut,lenOut,message] = interpretMessage(statusIn,eventIn,readOut)

% Check if running status
introValue = readOut(eventIn+1);
if isStatusByte(introValue)
    statusOut = introValue;         % New status
    running = false;
else
    statusOut = statusIn;           % Running status—Keep old status
    running = true;
end

switch statusOut
    case 255     % Meta-event (FF)—IGNORE
        [eventLength, lengthLen] = findVariableLength(eventIn+2, ...
            readOut);   % Meta-events have an extra byte for type of meta-event
        lenOut = 2+lengthLen+eventLength;
        message = -1;
    case 240     % Sysex message (F0)—IGNORE
        [eventLength, lengthLen] = findVariableLength(eventIn+1, ...
            readOut);
        lenOut = 1+lengthLen+eventLength;
        message = -1;
        
    case 247     % Sysex message (F7)—IGNORE
        [eventLength, lengthLen] = findVariableLength(eventIn+1, ...
            readOut);
        lenOut = 1+lengthLen+eventLength;
        message = -1;
    otherwise    % MIDI message—READ
        eventLength = msgnbytes(statusOut);
        if running  
            % Running msgs don't retransmit status—Drop a bit
            lenOut = eventLength-1;
            message = uint8([statusOut;readOut(eventIn+(1:lenOut))]);
            
        else
            lenOut = eventLength;
            message = uint8(readOut(eventIn+(1:lenOut)));
        end
end

end

% ----

function n = msgnbytes(statusByte)

if statusByte <= 191        % hex2dec('BF')
    n = 3;
elseif statusByte <= 223    % hex2dec('DF')
    n = 2;
elseif statusByte <= 239    % hex2dec('EF')
    n = 3;
elseif statusByte == 240    % hex2dec('F0')
    n = 1;
elseif statusByte == 241    % hex2dec('F1')
    n = 2;
elseif statusByte == 242    % hex2dec('F2')
    n = 3;
elseif statusByte <= 243    % hex2dec('F3')
    n = 2;
else
    n = 1;
end

end

% ----

function yes = isStatusByte(b)
yes = b > 127;
end

Создание MIDI-сообщений

midimsg объект может генерировать MIDI-сообщение из структуры с использованием формата:

midistruct = struct('RawBytes', [144 65 127 0 0 0 0 0], 'Timestamp',1);
msg = midimsg.fromStruct(midiStruct)

Это возвращает:

msg = 
  MIDI message:
    NoteOn          Channel: 1  Note: 65  Velocity: 127 Timestamp: 1  [ 90 41 7F ]

createMessage функция возвращает midimsg объект и метка времени. midimsg объект требует, чтобы его входная структура имела два поля:

  • RawBytes—Вектор байтов 1 на 8

  • Timestamp—Время в секундах

Для установки RawBytes , взять вектор байтов, созданный interpretMessage и добавьте достаточно нулей для создания вектора байтов 1 на 8.

Для установки Timestamp поле, создайте переменную временной метки ts. Набор ts до 0 перед синтаксическим анализом любых частей дорожки. Для каждого отправленного MIDI-сообщения преобразуйте значение дельта-времени из засечек в секунды. Затем добавьте это значение к ts. Чтобы преобразовать засечки MIDI в секунды, используйте:

timeAdd = numTicks • tempoticsPerQuarateNote • 1e6

Где темп находится в микросекундах (мкс) за квартальную заметку. Чтобы преобразовать количество ударов в минуту (BPM) в мкс в квартал, используйте:

tempo = 6e7BPM

После заполнения обоих полей структуры создайте midimsg объект. Вернуть midimsg объект и измененное значение ts.

createMessage функция игнорирует сообщения Sysex и мета-события. Когда interpretMessage обрабатывает сообщения Sysex и мета-события, возвращает -1 вместо вектора байтов. createMessage затем функция проверяет это значение. Если createMessage определяет сообщение Sysex или мета-событие, оно возвращает ts значение оно было дано и пустое midimsg объект.

function [tsOut,msgOut] = createMessage(messageIn,tsIn,deltaTimeIn,ticksPerQNoteIn,bpmIn)

if messageIn < 0     % Ignore Sysex message/meta-event data
    tsOut = tsIn;
    msgOut = midimsg(0);
    return
end

% Create RawBytes field
messageLength = length(messageIn);
zeroAppend = zeros(8-messageLength,1);
bytesIn = transpose([messageIn;zeroAppend]);

% deltaTimeIn and ticksPerQNoteIn are both uints
% Recast both values as doubles
d = double(deltaTimeIn);
t = double(ticksPerQNoteIn);

% Create Timestamp field and tsOut
msPerQNote = 6e7/bpmIn;
timeAdd = d*(msPerQNote/t)/1e6;
tsOut = tsIn+timeAdd;

% Create midimsg object
midiStruct = struct('RawBytes',bytesIn,'Timestamp',tsOut);
msgOut = midimsg.fromStruct(midiStruct);

end

Воспроизведение MIDI-сообщений с помощью синтезатора

В этом примере воспроизводятся проанализированные MIDI-сообщения с помощью простого монофонического синтезатора. Демонстрацию этого синтезатора см. в разделе Проектирование и воспроизведение синтезатора MIDI.

Вы также можете отправлять проанализированные MIDI-сообщения на устройство MIDI с помощью midisend. Дополнительные сведения о взаимодействии с устройствами MIDI с помощью MATLAB см. в разделе Интерфейс устройств MIDI.

function simplesynth(msgArray,osc,deviceWriter)

i = 1;
tic
endTime = msgArray(length(msgArray)).Timestamp;

while toc < endTime
    if toc >= msgArray(i).Timestamp     % At new note, update deviceWriter
        msg = msgArray(i);      
        i = i+1;
        if isNoteOn(msg)
            osc.Frequency = note2freq(msg.Note);
            osc.Amplitude = msg.Velocity/127;
        elseif isNoteOff(msg)
            if msg.Note == msg.Note
                osc.Amplitude = 0;
            end
        end
    end
    deviceWriter(osc());    % Keep calling deviceWriter as it is updated
end

end

% ----

function yes = isNoteOn(msg)
yes = strcmp(msg.Type,'NoteOn') ...
    && msg.Velocity > 0;
end

% ----

function yes = isNoteOff(msg)
yes = strcmp(msg.Type,'NoteOff') ...
    || (strcmp(msg.Type,'NoteOn') && msg.Velocity == 0);
end

% ----

function freq = note2freq(note)
freqA = 440;
noteA = 69;
freq = freqA * 2.^((note-noteA)/12);
end