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

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

  1. Считайте двоичный файл в рабочую область MATLAB ®.

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

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

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

Введение

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

Чтение файла MIDI

Чтение файла MIDI с помощью fread функция. The 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 находится в тактах деления на четверти ноты.

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

12562+22561+32560

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

Синтаксический анализ фрагмента MIDI Track

Фрагмент дорожки 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 Synthesizer.

% 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 Device Interface.

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

Чтение дельта-таймов

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

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

The 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

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

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

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

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

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

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

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

The 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

The 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 ]

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

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

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

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

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

timeAdd=numTickstempoticksPerQuarterNote1e6

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

tempo=6e7BPM

Заполнив оба поля struct, создайте midimsg объект. Верните midimsg объект и измененное значение ts.

The createMessage функция игнорирует сообщения Sysex и мета-события. Когда interpretMessage обрабатывает сообщения Sysex и мета-события, возвращается -1 вместо вектора байтов. The 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 Synthesizer.

Можно также отправить проанализированные сообщения MIDI на устройство MIDI с помощью midisend. Для получения дополнительной информации о взаимодействии с устройствами MIDI с помощью MATLAB, смотрите MIDI Device Interface.

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