В этом примере показано, как преобразовать обычные MIDI-файлы в представление сообщений MIDI с помощью Audio Toolbox™. В этом примере выполняется следующее:
Чтение двоичного MIDI-файла в рабочую область MATLAB ®.
Преобразование данных MIDI-файла в midimsg объекты.
Воспроизводите MIDI-сообщения на звуковой карте с помощью простого синтезатора.
Дополнительные сведения о взаимодействии с устройствами MIDI с помощью MATLAB см. в разделе Интерфейс устройств MIDI. Для получения дополнительной информации о MIDI в целом обратитесь в Ассоциацию производителей MIDI.
MIDI-файлы содержат MIDI-сообщения, информацию о времени и метаданные о кодированной музыке. В этом примере показано, как извлекать MIDI-сообщения и информацию о синхронизации. Чтобы упростить код, в этом примере метаданные игнорируются. Поскольку метаданные включают в себя такие сведения, как временная сигнатура и темп, в этом примере предполагается, что MIDI-файл находится в 4/4 времени со скоростью 120 ударов в минуту (BPM).
Чтение MIDI-файла с помощью fread функция. fread функция возвращает вектор байтов, представленный целыми числами.
readme = fopen('CmajorScale.mid');
[readOut, byteCount] = fread(readme);
fclose(readme);midimsg ОбъектыMIDI-файлы содержат блоки заголовков и блоков отслеживания. Блоки заголовков предоставляют основную информацию, необходимую для интерпретации остальной части файла. Файлы MIDI всегда начинаются с фрагмента заголовка. Блоки дорожки следуют за блоком заголовка. Блоки отслеживания предоставляют MIDI-сообщения, информацию синхронизации и метаданные файла. Каждый блок дорожки имеет заголовок блока дорожки, который включает в себя длину блока дорожки. Блок дорожек содержит события MIDI после заголовка блока дорожек. Каждое событие MIDI имеет дельта-время и сообщение MIDI.

Блок заголовка MIDI включает в себя разделение файла по времени. Разделение по времени определяет, как интерпретировать разрешение засечек в MIDI-файле. Галочки - это единица времени, используемая для установки меток времени для MIDI-файлов. MIDI-файл с большим количеством засечек в единицу времени содержит MIDI-сообщения с более детализированными отметками времени. Разделение по времени не определяет темп. MIDI-файлы определяют разделение по времени либо по галочкам в квартальной заметке, либо по кадрам в секунду. В этом примере предполагается, что разделение по времени MIDI находится в клещах в квартальной заметке.
fread функция считывает двоичные файлы по байтам, но разделение по времени сохраняется как 16-разрядное (2-байтовое) значение. Чтобы вычислить несколько байтов как одно значение, используйте polyval функция. Вектор байтов может быть оценен как многочлен, где x установлен на 256. Например, вектор байтов [1 2 3] может оцениваться как:
% Concatenate ticksPerQNote from 2 bytes
ticksPerQNote = polyval(readOut(13:14),256);Блок дорожек 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.
% 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-файлах существует три основных типа сообщений:
Сообщения 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
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 в секунды, используйте:
• 1e6
Где темп находится в микросекундах (мкс) за квартальную заметку. Чтобы преобразовать количество ударов в минуту (BPM) в мкс в квартал, используйте:
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 с помощью 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