Convert MIDI Files into MIDI Messages
This example shows how to convert ordinary MIDI files into MIDI message representation using Audio Toolbox™. In this example, you:
Read a binary MIDI file into the MATLAB® workspace.
Convert the MIDI file data into
midimsg
objects.Play the MIDI messages to your sound card using a simple synthesizer.
For more information about interacting with MIDI devices using MATLAB, see MIDI Device Interface. To learn more about MIDI in general, consult The MIDI Manufacturers Association.
Introduction
MIDI files contain MIDI messages, timing information, and metadata about the encoded music. This example shows how to extract MIDI messages and timing information. To simplify the code, this example ignores metadata. Because metadata includes information like time signature and tempo, this example assumes the MIDI file is in 4/4 time at 120 beats per minute (BPM).
Read MIDI File
Read a MIDI file using the fread
function. The fread
function returns a vector of bytes, represented as integers.
readme = fopen('CmajorScale.mid');
[readOut, byteCount] = fread(readme);
fclose(readme);
Convert MIDI Data into midimsg
Objects
MIDI files have header chunks and track chunks. Header chunks provide basic information required to interpret the rest of the file. MIDI files always start with a header chunk. Track chunks come after the header chunk. Track chunks provide the MIDI messages, timing information, and metadata of the file. Each track chunk has a track chunk header that includes the length of the track chunk. The track chunk contains MIDI events after the track chunk header. Every MIDI event has a delta-time and a MIDI message.
Parse MIDI Header Chunk
The MIDI header chunk includes the timing division of the file. The timing division determines how to interpret the resolution of ticks in the MIDI file. Ticks are the unit of time used to set timestamps for MIDI files. A MIDI file with more ticks per unit time has MIDI messages with more granular time stamps. Timing division does not determine tempo. MIDI files specify timing division either by ticks per quarter note or frames per second. This example assumes the MIDI timing division is in ticks per quarter note.
The fread
function reads binary files byte-by-byte, but the timing division is stored as a 16-bit (2-byte) value. To evaluate multiple bytes as one value, use the polyval
function. A vector of bytes can be evaluated as a polynomial where x is set at 256. For example, the vector of bytes [1 2 3]
can be evaluated as:
% Concatenate ticksPerQNote from 2 bytes
ticksPerQNote = polyval(readOut(13:14),256);
Parse MIDI Track Chunk
The MIDI track chunk contains a header and MIDI events. The track chunk header contains the length of the track chunk. The rest of the track chunk contains one or more MIDI events.
All MIDI events have two main components:
A delta-time value—The time difference in ticks between the previous MIDI track event and the current one
A MIDI message—The raw data of the MIDI track event
To parse MIDI track events sequentially, construct a loop within a loop. In the outer loop, parse track chunks, iterating by chunkIndex
. In the inner loop, parse MIDI events, iterating by a pointer ptr
.
To parse MIDI track events:
Read the delta-time value at a pointer.
Increment the pointer to the beginning of the MIDI message.
Read the MIDI message and extract the relevant data.
Add the MIDI message to a MIDI message array.
Display the MIDI message array when complete.
% 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 ]
Synthesize MIDI Messages
This example plays parsed MIDI messages using a simple monophonic synthesizer. To see a demonstration of this synthesizer, see Design and Play a MIDI Synthesizer.
% Initialize System objects for playing MIDI messages osc = audioOscillator('square', 'Amplitude', 0,'DutyCycle',0.75); deviceWriter = audioDeviceWriter; simplesynth(msgArray,osc,deviceWriter);
You can also send parsed MIDI messages to a MIDI device using midisend
. For more information about interacting with MIDI devices using MATLAB, see MIDI Device Interface.
Helper Functions
Read Delta-Times
The delta-times of MIDI track events are stored as variable-length values. These values are 1 to 4 bytes long, with the most significant bit of each byte serving as a flag. The most significant bit of the final byte is set to 0, and the most significant bit of every other byte is set to 1.
In a MIDI track event, the delta-time is always placed before the MIDI message. There is no gap between a delta-time and the end of the previous MIDI event.
The findVariableLength
function reads variable-length values like delta-times. It returns the length of the input value and the value itself. First, the function creates a 4-byte vector byteStream
, which is set to all zeros. Then, it pushes a pointer to the beginning of the MIDI event. The function checks the four bytes after the pointer in a loop. For each byte, it checks the most significant bit (MSB). If the MSB is zero, findVariableLength
adds the byte to byteStream
and exits the loop. Otherwise, it adds the byte to byteStream
and continues to the next byte.
Once the findVariableLength
function reaches the final byte of the variable-length value, it evaluates the bytes collected in byteStream
using the polyval
function.
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
Interpret MIDI Messages
There are three main types of messages in MIDI files:
Sysex messages — System-exclusive messages ignored by this example.
Meta-events — Can occur in place of MIDI messages to provide metadata for MIDI files, including song title and tempo. The
midimsg
object does not support meta-events. This example ignores meta-events.MIDI messages — Parsed by this example.
To interpret a MIDI message, read the status byte. The status byte is the first byte of a MIDI message.
Even though this example ignores Sysex messages and meta-events, it is important to identify these messages and determine their lengths. The lengths of Sysex messages and meta-events are key to determining where the next message starts. Sysex messages have 'F0'
or 'F7'
as the status byte, and meta-events have 'FF'
as the status byte. Sysex messages and meta-events can be of varying lengths. After the status byte, Sysex messages and meta-events specify event lengths. The event length values are variable-length values like delta-time values. The length of the event can be determined using the findVariableLength
function.
For MIDI messages, the message length can be determined by the value of the status byte. However, MIDI files support running status. If a MIDI message has the same status byte as the previous MIDI message, the status byte can be omitted. If the first byte of an incoming message is not a valid status byte, use the status byte of the previous MIDI message.
The interpretMessage
function returns a status byte, a length, and a vector of bytes. The status byte is returned to the inner loop in case the next message is a running status message. The length is returned to the inner loop, where it specifies how far to push the inner loop pointer. Finally, the vector of bytes carries the raw binary data of a MIDI message. interpretMessage
requires an output even if the function ignores a given message. For Sysex messages and meta-events, interpretMessage
returns -1
instead of a vector of bytes.
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
Create MIDI Messages
The midimsg
object can generate a MIDI message from a struct using the format:
midistruct = struct('RawBytes', [144 65 127 0 0 0 0 0], 'Timestamp',1); msg = midimsg.fromStruct(midiStruct)
This returns:
msg = MIDI message: NoteOn Channel: 1 Note: 65 Velocity: 127 Timestamp: 1 [ 90 41 7F ]
The createMessage
function returns a midimsg
object and a timestamp. The midimsg
object requires its input struct to have two fields:
RawBytes—
A 1-by-8 vector of bytesTimestamp—
A time in seconds
To set the RawBytes
field, take the vector of bytes created by interpretMessage
and append enough zeros to create a 1-by-8 vector of bytes.
To set the Timestamp
field, create a timestamp variable ts
. Set ts
to 0 before parsing any track chunks. For every MIDI message sent, convert the delta-time value from ticks to seconds. Then, add that value to ts
. To convert MIDI ticks to seconds, use:
Where tempo is in microseconds (μs) per quarter note. To convert beats per minute (BPM) to μs per quarter note, use:
Once you fill both fields of the struct, create a midimsg
object. Return the midimsg
object and the modified value of ts
.
The createMessage
function ignores Sysex messages and meta-events. When interpretMessage
handles Sysex messages and meta-events, it returns -1
instead of a vector of bytes. The createMessage
function then checks for that value. If createMessage
identifies a Sysex message or meta-event, it returns the ts
value it was given and an empty midimsg
object.
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
Play MIDI Messages Using a Synthesizer
This example plays parsed MIDI messages using a simple monophonic synthesizer. To see a demonstration of this synthesizer, see Design and Play a MIDI Synthesizer.
You can also send parsed MIDI messages to a MIDI device using midisend
. For more information about interacting with MIDI devices using MATLAB, see 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