%% 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:
% 1\bullet {256}^2 +2\bullet {256}^1 +3\bullet {256}^0

% 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)

%
%% 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 bytes
% Timestamp—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:
% \textrm{timeAdd}=\frac{\textrm{numTicks}\bullet \textrm{tempo}}{\textrm{ticksPerQuarterNote}\bullet 1\textrm{e6}}
% Where tempo is in microseconds (μs) per quarter note. To convert beats per minute (BPM) to μs per quarter note, use:
% \textrm{tempo}=\;\frac{6\mathrm{e}7}{\textrm{BPM}}
% 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

% Copyright 2018 The MathWorks, Inc.