# umidiparser - MIDI file parser for Micropython

## DESCRIPTION
This module reads MIDI files (SMF files) and gets all the MIDI events contained in the file. It also can return each event at the corresponding time.

Example:

    import umidiparser
    import utime
    for event in umidiplay.MidiFile("example.mid").play():
        # .play will sleep, avoiding time drift, before returning the event on time
        # Process the event according to type
        if event.status == umidiplay.NOTE_ON:
            ... start the note with midi number event.note 
		on channel event.channel with event.velocity
        elif event.status == umidiplay.NOTE_OFF :
            ... stop the note event.note .
        elif event.status == umidiplay.PROGRAM_CHANGE:
            ... change midi program to event.program on event.channel ....
        else:
           # Show all events not processed
           print("other event", event )


This module does not contain a sound synthesizer, only the capabilities to
read and interpret a MIDI file.

Memory and CPU usage is optimized for a microcontroller with limited resources with Micropython. 
CPU and memory usage can be lowered even more by reusing the same MidiEvent over and over
during the process:

    for event in MidiFile("example.mid", reuse_event_object=True ):
        ... process event....

Normally the complete midi file will be buffered in RAM when opening the
file. If RAM is scarce or may be fragmented, use:

    for event in MidiFile("example.mid", buffer_size=100 ):
        .... process event...

This will allocate *buffer\_size* bytes as a buffer for each track in the
midi file, open one file descriptor for each track and then read the file
piecemeal. CPU usage may increase about 10% slightly (about 10%) but RAM usage
is limited to the *buffer\_size* bytes per track, and you can process a files
much larger than the available heap or RAM. See "CPU AND MEMORY" below.

This module will calculate the time between events, using the parameters and events available in the MIDI file (pulses per beat field in the file header and set tempo events).

## INSTALLATION

On CPython: pip install umidiparser
On Micropython: upip install umidiparser

You also can download the source package, get the umidiparser.py file and copy that to the folder where main.py is.


## MIDI FILE COMPATIBILITY
The parser will parse MIDI files, also known as SMF files, with format type 0, 1 and 2. 
Running status events are
recognized. All standard MIDI channel events (note on, note off,
program change, control change, etc) and meta and sysex events (track name,
key signature, time signature, etc) recognized. This module is based on the MIDI file standard version 1.1

For multitrack file type 1 files, tracks are automatically
merged during playback.

MIDI file format errors such as running status event at the beginning of
a track or incorrect file header fields are detected insofar they could
lead to wrong results. Non standard data chunks are ignored. Missing
end of track events are inserted on the fly.
Running status events may have a meta or sysex event in between, as this
is non standard but common in MIDI files.
Non standard or custom meta messages are recognized. Values of fields out
of range, such as note values between 128 and 255 are passed to the calling
program.

There are no capabilities to modify or create MIDI files.

## CPU AND MEMORY (RAM) USAGE
Parsing on a ESP32 at 200 Mhz CPU clock with Micropython takes about
one millisecond per event, and even less if you use the reuse_event_object
of the MidiFile object.

With the reuse_event_object parameter set to true, CPU overhead should be
about 15% lower, and less heap is used.
On the ESP32, parsing a MIDI file with buffer_size=100 and
reuse_event_object=True uses about 20kbytes of heap. Each event will
allocate and free less than 100 bytes of heap, so garbage collections are not
frequent if little RAM is available.
Most of the processing such as parsing event data into individual fields
is delayed until the information is really needed.

Frequently used methods have the @micropython.native decorator for speed.

## PORTABILITY
The code is optimized for Micropython, but
also works with same functionality under regular Python (CPython).
This allows to develop or test on Linux, Mac or Windows and then port to
Micropython. Not tested on CircuitPython so far.

## CLASSES
The starting point is the MidiFile object, allowing to iterate through
the events. Events are returned as MidiEvent objects, and the fields
of the event are visible as attributes of the MidiEvent.

## DEPENDENCIES
Imports *time/utime*. Provides a wrapper to work with CPython "import time".
Imports *os* to get the absolute path of the MIDI file.


# Class MidiFile

Parses a MIDI file.
Once initialized, you can iterate through the events of the file or
through the events of a certain track, see the iterator and the *play* method.

## MidiFile constructor parameters

### filename

The name of a MIDI file, usually a .mid or .rtx MIDI file.
The MIDI file will always be opened read only.

### buffer_size=100

The buffer size that will be allocated for each track. a value of zero means copy all tracks to RAM during processing.The default value is 100 bytes.

In order to process files larger than the available RAM,
*buffer_size=n* will allocate *n* bytes of buffer for
each track, and read each tracks in *n* byte portions during the
processing of the file, i.e. while iterating through the events. One file descriptor (one open file) will be used for each track, but the total RAM needed will be about *buffer_size* multiplied by the number of tracks of the MIDI file.

MIDI file format 0 files have only one track, so only one buffer will be allocated. 

A buffer size of less than 10 bytes will increase CPU overhead. A buffer size much larger than 100 will probably not give a
relevant performance advantage, unless the device where the file
resides is slow.

If *buffer_size=0^, all tracks will be read to memory.
This will need as much RAM as the file size of a complete MIDI file.
In this case, the time needed to process each event will not depend on
file access and will be both faster and more dependable than using a
*buffer_size* different to 0, if the device where the file resides is slow.

The default value for *buffer_size* is 100 bytes.

* reuse_event_object=False

With the default value of False, for each step of the iteration a new MidiEvent is returned,
this is, if you do:

            for event in MidiFile("example.mid"):
                ... process each event ...

then in each iteration of the loop you get a new MidiEvent object.
This is the normal and expected behavior for Python iterators.

However, if you if you have little RAM or need higher processing speed use:

    for event in MidiFile("example.mid", reuse_event_object=True):
         ... process each event ....

In this case, for each iteration of the loop, the same MidiEvent object is returned
For each step of the loop, but it is overwritten with the new data. However, if you want to store an event
for later use,  you'll have to make a deep copy using *event.copy*.

All combinations of *reuse_event_object* and *buffer_size* are valid.

Exceptions:

The file must start with a standard MIDI file header (MThd), if not, a
ValueError is raised.
The MIDI header chunk must be at least 6 bytes long, or a ValueError
is raised.
ValueError is raised if no header present, or too short.
ValueError is raised if the header contains SMPTE time codes (not supported).
Chunks after the header that are not tracks identified with the Mark headerare ignored.


## MidiFile Methods

### Iterating through the events of a MIDI file

To get all the events of a format type 0 or format type 1 MIDI file,
iterate through the MidiFile object, for example:
    
    import umidiparser
    for event in MidiFile("example.mid"):
        print(event)


Events will be returned in ascending order in time. For format type 1
multitrack files, all tracks are merged.

The *event.delta_us* property is calculated as the
time in microseconds between last and this event, taking into account both
the set tempo meta events and the "MIDI ticks per quarter note" (also known as "pulses per beat")
parameter in the MIDI file header. For type 1 files, all set tempo meta
events may be in track 0 (as it is usually done), or they may occur in any track.

For a multitrack file, *event.delta_us^ is the time difference with the
previous event, which may be in the same or a different track.

For a format type 2 MIDI file, a single track must be selected, see MidiFile.play method.

To get each event at the proper time of the event, use the 
*MidiFile.play* method. This method compensates for the delays during processing.

In all cases, only one end of track meta event is returned
to the caller when the end of file is reached.
Events beyond an end of track event are ignored.

Exceptions:

A *RuntimeError* is raised if format type 2 is processed with this method.
Use the *MidiFile.tracks[n] object to iterate through a single track.

### play

Iterate through the events of a MIDI file or a track,
sleep until the event has to take place, and
yield the event.

Example:

    for event in MidiFile("example.mid").play():
         .... process the event ...

The play function will wait the necessary time between iterations
so that each event is yielded on time to be processed.

This function compensates for the accumulated error in the
processing of each event. Since sleep functions will sleep
AT LEAST the time specified, normally the time slept will be longer. This
means, for a long file of several thousand events, events may get ever later.
This is noticeable especially 
if the processing of each event takes significant time.
Even so, since play uses the sleep_us function, sometimes
you may get the event a bit later than the correct time.

For Micropython, *time.sleep_us()^ is used. For CPython *time.sleep()* is used.

#### MidiFile play method parameters

###### track_number=None

If track_number=None, and the MIDI file is format 0 or 1,
play the complete file. Merge all tracks.

If a track number is specified, then that track number is played. This
is intended for use with format type 2 files, to play a certain track. Format 2 Midi files are not very common.

### length_us

Computes and returns the length of the MidiFile in microseconds.

Be aware that on a microcontroller, calculating the length of
a large MIDI file might take a several seconds.
This is due to the way MIDI files work. In order to
get the playing length, this method needs to parse
the entire file, compute and add the time differences of all events.

Exceptions:

*RuntimeError* for format type 2 files. It is not possible to calculate the
playing time of a format 2 file, since for format 2 files,
the tracks are not meant to be merged.

       

## MidiFile properties

The properties cannot be changed, they are set with the MidiFile constructor.

### buffer_size

Returns the *buffer_size value*. 
        
### filename

Returns the file name of the MIDI file, with absolute path. 
        
### format_type

Returns the MIDI format type as stored in the header of the MIDI file:

* format_type 0 files are single track MIDI files.
To parse a type 0 file, use the MIdiFile object as iterator:

    for event in MidiFile("example.mid"):
        ...process each event

* format_type 1 files are multitrack MIDI files. During playback, this module will merge the tracks
into an ordered sequence of events.

To parse a type 1 file, use the same code as with a type 0 file. 

* format_type 2 files are multitrack files where each track behaves like a format 0 single track MIDI file. Merging
tracks is not allowed. Track number "n" can be  parsed as follows:

    for event in MidiFile("format2file.mid").tracks[n]:
         .... process event...

Or also:

    For event in MidiFile("format2file.mid").play( track_number=n ):
         .... process event...

        
### miditicks_per_quarter

Return the MIDI ticks per quarter note (also called pulses per beat) field of the MIDI file header.
        
        
### reuse_event_object

Return the value of the reuse_event_object property.

## MidiFile instance variables
### tracks
List of MidiTrack objects, one for each track.

        
# Class MidiEvent

Represents a parsed MIDI event. You get MidiEvent objects iterating through a MidiFile.

## Properties available for all events

### status

This is the event type, such as note on, note off,
set tempo, end of track, sysex. You can compare event.status
with the constants umidiparser.NOTE_ON, umidiparser.NOTE_OFF,
umidiparser.PROGRAM_CHANGE, umidiparser.LYRICS, umidiparser.SYSEX, etc.

Available for all events types. 

Example:

    if event.status == umidiparser.NOTE_OFF:
        .... process note off event ...
     elif event.status == umidiparser.KEY_SIGNATURE:
          ... process key signature meta event ...

Possible values are constants in umidiparser:
* Channel messages
- NOTE_OFF
- NOTE_ON
- POLYTOUCH
- CONTROL_CHANGE
- PROGRAM_CHANGE
- AFTERTOUCH
- PITCHWHEEL

* Meta messages 
- SEQUENCE_NUMBER
- TEXT
- COPYRIGHT
- TRACK_NAME
- INSTRUMENT_NAME
- LYRICS
- MARKER
- CUE_MARKER
- PROGRAM_NAME
- DEVICE_NAME
- CHANNEL_PREFIX
- MIDI_PORT
- END_OF_TRACK
- SET_TEMPO
- SMPTE_OFFSET
- TIME_SIGNATURE
- KEY_SIGNATURE
- SEQUENCER_SPECIFIC

 * Sysex/escape events
 - SYSEX
 - ESCAPE



### delta_us

Time in microseconds since the last event for this
event to start. For example, you might use sleep_us( event.delta_us ) to
sleep the appropriate time for the event to start. This module calculates delta_us using the delta in midi ticks, the pulses per quarter in the MIDI file header and the set tempo events in the file.

### delta_miditicks

Difference, in MIDI ticks (MIDI pulses) between last event
and this event. This number is also known as "delta time" of the event.
For single track files, the time difference is with the previous event
of the same track. When parsing multitrack files, tracks are merged and this
time is set during playback to the time difference with the previous event, which may or may not be in the same track.

### data 
Contains the raw event data.


## Event specific properties

For each type of event, only the applicable properties are enabled.
The following list shows the event status and properties
that you can get.

For example: for a note_on event, event.status is umidiparser.NOTE_ON
and the properties event.note, event.channel, and event.velocity return the 
values of the event.

Attributes that are not available throw an AttributeError on access.

## MIDI channel event specific properties

* NOTE_OFF (midi event status byte=0x80 to 0x8f)
    - channel (int)
    - note (int)
    - velocity (int)
* NOTE_ON (midi event status byte=0x90 to 0x9f)
    - channel (int)
    - note (int)
    - velocity (int)
* POLYTOUCH (midi event status byte=0xa0 to 0xaf)
    - channel (int)
    - note (int)
    - value (int)
* CONTROL_CHANGE (midi event status byte=0xb0 to 0xbf)
    - channel (int)
    - control (int)
    - value (int)
* PROGRAM_CHANGE (midi event status byte=0xc0 to 0xcf)
    - channel (int)
    - program (int)
* AFTERTOUCH (midi event status byte=0xd0 to 0xdf)
    - channel (int)
    - value (int)
* PITCHWHEEL (midi event status byte=0xe0 to 0xef)
    - channel (int)
    - pitch (int)

## Meta and Sysex event specific properties
* SEQUENCE_NUMBER (midi meta 0xff 0x00)
    - number (int)
* TEXT (midi meta 0xff 0x01)
    - text (str)
* COPYRIGHT (midi meta 0xff 0x02)
    - text (str)
* TRACK_NAME (midi meta 0xff 0x03)
    - name (str)
* INSTRUMENT_NAME (midi meta 0xff 0x04)
    - name (str)
* LYRICS (midi meta 0xff 0x05)
    - text (str)
* MARKER (midi meta 0xff 0x06)
    - text (str)
* CUE_MARKER (midi meta 0xff 0x07)
    - text (str)
* PROGRAM_NAME (midi meta 0xff 0x08)
    - name (str)
* DEVICE_NAME (midi meta 0xff 0x09)
    - name (str)
* CHANNEL_PREFIX (midi meta 0xff 0x20)
    - channel (int)
* MIDI_PORT (midi meta 0xff 0x21)
    - port (int)
* END_OF_TRACK (midi meta 0xff 0x2f)
    - no event specific attributes available
* SET_TEMPO (midi meta 0xff 0x51)
    - tempo (int)
* SMPTE_OFFSET (midi meta 0xff 0x54)
    - frame_rate (int)
    - frames (int)
    - hours (int)
    - minutes (int)
    - seconds (int)
    - sub_frames (int)
* TIME_SIGNATURE (midi meta 0xff 0x58)
    - clocks_per_click (int)
    - denominator (int)
    - notated_32nd_notes_per_beat (int)
    - numerator (int)
* KEY_SIGNATURE (midi meta 0xff 0x59)
    - key (str)
* SEQUENCER_SPECIFIC (midi meta 0xff 0x7f)
    - data (memoryview)

Text strings are decoded with the latin-1 code page. If a Midi file should have text with another encoding, use the data property and decode with the appropriate code page.

* SYSEX  0xf0
    - data (memoryview)
* ESCAPE  0xf7
    - data (memoryview)

## MidiEvent methods

### copy

Returns a deep copy (a complete independent copy) of the event as a new object.

This can be useful to get a copy of the event in case of using
the reuse_event_object=True option in the MidiFile.

Example 1: if you need a list of all MidiEvents
in a file, then either use:

    event_list = [event.copy for event in MidiFile("example.mid",
        reuse_event_object=True) ]

or use:

    event_list = [ event for event in MidiFile("example.mid") ]
        
### is_meta

Returns True if this is a Meta event, such as
lyrics, set tempo or key signature.

Returns False if this is a MIDI channel event,
or a Sysex or Escape event.

### str(event) 
Will translate the event information to a string, for example:

    print(event)    
    event_as_a_string = str(event)

Returns a string with a description of the MidiEvent, starting
with the event name in lowercase (note_on, note_off, pitchwheel, set_tempo,
end_of_track, etc), delta time in midi ticks, delta time in microseconds,
first few bytes of the raw data for meta events, and all properties
that are allowed for the event type. For example, a "note on" event might
be shown as:

"note_on delta[miditicks]=10 delta[usec]=9500 note=60 velocity=64 channel=5"

The order of the properties in the string may vary. 


## MidiEvent properties
### channel

Returns the channel number for the event, 0-15.

channel property is available for:  NOTE_OFF NOTE_ON
POLYTOUCH CONTROL_CHANGE PROGRAM_CHANGE AFTERTOUCH
CHANNEL_PREFIX
        
### clocks_per_click

Returns the clocks_per_click for the TIME_SIGNATURE meta messages, 0-255.
        
### control

Returns the value for the controller 0-127 for a CONTROL_CHANGE event.
        
### data

Returns the raw data for the underlying message, with no transofrmations,
as a memoryview, without the event status byte or meta prefix.

For midi channel events, the length is either 1 or 2 bytes
according to the event type, for example a "note on" event always
has 2 bytes of data.
For a meta or sysex event, "data" contains the payload of the message,
that is, without meta prefix and length.
For sysex and escape events, the status (0xf0, xf7) is not included.

The main purpose is to retrieve message data for sysex and escape events.
        
### denominator

Returns the denominator for the TIME_SIGNATURE meta messages, 0-255.
        
### frame_rate

Returns the frame for the SMPTE_OFFSET meta messages,
which can be 24, 25, 29.97 or 30.

An invalid value in the MIDI file will raise a *IndexError*
        
### frames

Returns the frames for the SMPTE_OFFSET meta message,
usually from 0 to 255.
        
### hours

Returns the hour for the SMPTE_OFFSET meta message,
usually from 0 to 23.
        
### key

Returns the key, as string, for a KEY_SIGNATURE meta event.
  
For mayor keys:
C, D, E, F, G, A, B, C#, F#, Cb, Db, Eb, Gb, Ab

For minor keys:
Cm, Dm, Em, Fm, Gm, Am, Bm, C#m, F#m, Cbm, Dbm, Ebm, Gbm, Abm

If the midi message contains a value out of range, a *ValueError*
is raised. The raw data can be accessed with the data property.
        
### minutes

Returns the minutes for the SMPTE_OFFSET meta message,
usually from 0 to 59.
        
### name

Returns the *name* data field for certain meta events.

name property is available for:  TRACK_NAME INSTRUMENT_NAME PROGRAM_NAME DEVICE_NAME

See text property for description of text decoding.

The raw data can be retrieved using the data property.
        
### notated_32nd_notes_per_beat

Returns the notated_32nd_notes_per_beat for the TIME_SIGNATURE meta messages,
0-255.
        
### note

Returns the note number for the event, usually 0-127.

note property available for:  NOTE_OFF NOTE_ON POLYTOUCH
        
### number

Returns number of a SEQUENCE_NUMBER meta event.
Values range from 0 to 2**24.
        
### numerator

Returns the numerator for the TIME_SIGNATURE meta messages, 0-255.
        
### pitch

Returns the pitch for a PITCHWHEEL midi channel event.

-8192 is the lowest value possible, 0 (zero) means "no pitch bend"
and 8191 is the highest value possible.
        
### port

Returns the port number  0-256 for a meta MIDI_PORT message
        
### program

Returns the program number 0-127 for a PROGRAM_CHANGE event.
        
### seconds

Returns the seconds for the SMPTE_OFFSET meta message,
usually from 0 to 59.        
       
### sub_frames

Returns the sub frames for the SMPTE_OFFSET meta message,
usually from 0 to 59.
        
### tempo

Returns the tempo in microseconds per quarter beat
for a SET_TEMPO meta event.
This module interprets the tempo event before returning it, so
the following events returned will have their *delta_us* property
calculated with the new tempo value.

        
### text

Returns the text for a meta events.

text property is available for:  TEXT COPYRIGHT LYRICS MARKER CUE_MARKER

In MIDI files, text is stored as "extended ASCII", this is
decoded with the *latin-1* encoding.

The raw data can be retrieved using the data property.
        
### to_midi

Returns the event as bytes, in a format that allows sending the
data to a MIDI controller.

to_midi will raise AttributeError if the event is for MIDI meta messages, these
occur in MIDI files and are not normally sent to MIDI controllers.
        
### value

Returns the the value in the event.

value property available for:  AFTERTOUCH, CONTROL_CHANGE, POLYTOUCH
        
### velocity

Returns the velocity for the event, usually 0-127.

velocity property available for:  NOTE_OFF NOTE_ON

## AUTHOR
Hermann Paul von Borries

## LICENSE
MIT, Copyright (c) 2022 Hermann Paul von Borries


        
