Преобразуйте файлы 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] может быть оценен как:

12562+22561+32560

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

Проанализируйте фрагмент дорожки MIDI

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

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

  • Разовое дельтой значение — разница во времени в метках деления между предыдущими беговыми соревнованиями MIDI и текущим

  • Сообщение MIDI — необработанные данные беговых соревнований MIDI

Чтобы проанализировать беговые соревнования 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-устройства.

Функции помощника

Считайте времена Delta

Времена дельты беговых соревнований 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 от struct с помощью формата:

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 объект требует, чтобы его входной struct имел два поля:

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

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

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

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

timeAdd=numTickstempoticksPerQuarterNote1e6

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

tempo=6e7BPM

Если вы заполняете оба поля struct, создаете 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