Преобразуйте файлы 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=numTicksтемпticksPerQuarterNote1e6

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

темп=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