В этом примере показано, как преобразовать обычные файлы MIDI в представление сообщений MIDI с помощью Audio Toolbox™. В этом примере вы:
Считайте двоичный файл в рабочую область MATLAB ®.
Преобразуйте данные файла MIDI в midimsg
объекты.
Воспроизведение сообщений MIDI на звуковой карте с помощью простого синтезатора.
Для получения дополнительной информации о взаимодействии с устройствами MIDI с помощью MATLAB, смотрите MIDI Device Interface. Чтобы узнать больше о MIDI в целом, обратитесь в Ассоциацию производителей MIDI.
MIDI файлов содержать сообщения MIDI, время выполнения и метаданные о закодированной музыке. В этом примере показано, как извлечь сообщения MIDI и время выполнения. Чтобы упростить код, этот пример игнорирует метаданные. Поскольку метаданные включают информацию, подобную сигнатуре времени и темпу, этот пример предполагает, что файл находится в 4/4 времени со скоростью 120 ударов в минуту (BPM).
Чтение файла MIDI с помощью fread
функция. The fread
функция возвращает вектор байтов, представленный в виде целых чисел.
readme = fopen('CmajorScale.mid');
[readOut, byteCount] = fread(readme);
fclose(readme);
midimsg
ОбъектыMIDI- файлов имеют фрагменты заголовка и отслеживают фрагменты. Фрагменты заголовка обеспечивают базовую информацию, необходимую для интерпретации остальной части файла. MIDI файлов всегда начинаться с фрагмента заголовка. Отслеживайте фрагменты приходят после фрагмента заголовка. Отслеживайте фрагменты предоставляйте сообщения MIDI, время выполнения и метаданные файла. Каждый фрагмент дорожки имеет заголовок фрагмента дорожки, который включает в себя длину фрагмента дорожки. Фрагмент дорожки содержит события MIDI после заголовка фрагмента дорожки. Каждое событие MIDI имеет дельта-время и сообщение MIDI.
Заголовок MIDI фрагмента включает в себя деление синхронизации файла. Временное деление определяет, как интерпретировать разрешение тактов в файле. Такты - этот модуль времени, используемая для установки временных меток для файлов MIDI. MIDI- файла с большими тактами в модуль времени имеет сообщения MIDI с более гранулярными метками времени. Временное деление не определяет темп. MIDI файлов задать временное деление либо на такты в квартал, либо на системы координат в секунду. Этот пример предполагает, что временное деление MIDI находится в тактах деления на четверти ноты.
The 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 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
В файлах существует три основных типа сообщений:
Сообщения 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
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 в секунды, используйте:
Где темп в микросекундах (мкс) на четверть ноты. Для преобразования ударов в минуту (BPM) в мкс в четверть ноты, используйте:
Заполнив оба поля 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 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