Music projects

MIDI files: Analysing Standard MIDI files (SMF)

Last updated: 2025-07-21

Quick links

Intro

In my DRUMPPY software I want to save the patterns as MIDI files. So better to understand MIDI files. I already did some bytewise analysing back in 2008 and documented it in German. So here I will reuse this knowledge but also the cool MIDO Python library that facilitates the analysing.

midifile

The Standard MIDI File SMF

Standard MIDI commands (messages) and time stamps are saved in the standardised MIDI file (SMF, ending ".mid"). Data required by sequencers (meta messages like tempo, time signature, information about the scale, etc.) can also be saved.

It is the lowest common denominator for storing music data, and allows the exchange between all kinds of programmes, DAWs, sequencers etc..

Several tracks (one voice, e.g. clarinet) and several patterns (possibly consisting of several tracks that all start at the same time) can be saved. Data is stored in so-called "chunks" (units). Such a chunk consists of a unique 4 byte ID (ASCII: "MThd or MTrk"), the length of following data bytes (4 byte) and the data. Proprietary data can also be sent in a unit, which is ignored by other devices.

Standard MIDI file formats

There are 3 types of MIDI files:

Important terms to know relating to timing

This can be confusing, so her e a little overview:

Most important for a MIDI file are the time base (PPQN and ticks) and the tempo. These are 2 independent variables!

Analysing an SMF with the Python MIDO library

I created 3 MIDI files with a basic rock pop drum pattern (Downloads).

pattern
click for a better view

Two where created with hydrogen and one with rosegarden. They where exported from the programs to MIDI files. Hydrogen allows to export the file in type 0 or in type 1. If we save the Hydrogen project (simple_drum_pattern.h2song, 67.2 kB) or the Rosegarden file (simple_drum_pattern.rg, 9.7 kB) and compare the file sizes with the MIDI files (187 bytes, 296 bytes), we see that in a software project much more is saved than in the MIDI file (lowest common denominator for storing music data :)).

The code to analyse the files is short.

    from mido import MidiFile

    mfile = MidiFile("simple_drum_pattern_hydrogen_t0.mid")
    print(f"File format: {mfile.type}")
    for i, track in enumerate(mfile.tracks):
      print(f'=== Track {i}')
      for message in track:
          print(f'  {message!r}')

First we look at the output of the type 0 file created with hydrogen:

    File format: 0
    === Track 0
      MetaMessage('copyright', text='(C) hydrogen 2025', time=0)
      MetaMessage('track_name', name='Untitled Song', time=0)
      MetaMessage('set_tempo', tempo=500000, time=0)
      MetaMessage('time_signature', numerator=4, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8, time=0)
      Message('note_on', channel=9, note=36, velocity=101, time=0)
      ...
      Message('note_off', channel=9, note=42, velocity=101, time=48)
      MetaMessage('end_of_track', time=0)

We see that there is only one track (Type 0) and we get 4 meta messages.

The output for the type 1 file:

    File format: 1
    === Track 0
      MetaMessage('copyright', text='(C) hydrogen 2025', time=0)
      MetaMessage('track_name', name='Untitled Song', time=0)
      MetaMessage('set_tempo', tempo=500000, time=0)
      MetaMessage('time_signature', numerator=4, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8, time=0)
      MetaMessage('end_of_track', time=0)
    === Track 1
      Message('note_on', channel=9, note=36, velocity=101, time=0)
      ...
      Message('note_off', channel=9, note=42, velocity=101, time=48)
      MetaMessage('end_of_track', time=0)

and the output for the file created with Rosegarden:

    File format: 1
    === Track 0
      MetaMessage('copyright', text='Copyright (c) xxxx Copyright Holder', time=0)
      MetaMessage('cue_marker', text='Created by Rosegarden', time=0)
      MetaMessage('cue_marker', text='http://www.rosegardenmusic.com/', time=0)
      MetaMessage('set_tempo', tempo=500000, time=0)
      MetaMessage('time_signature', numerator=4, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8, time=0)
      MetaMessage('end_of_track', time=192000)
    === Track 1
      MetaMessage('track_name', name='test', time=0)
      Message('control_change', channel=9, control=121, value=0, time=0)
      Message('control_change', channel=9, control=10, value=64, time=0)
      Message('control_change', channel=9, control=93, value=0, time=0)
      Message('control_change', channel=9, control=7, value=100, time=0)
      Message('control_change', channel=9, control=91, value=0, time=0)
      Message('control_change', channel=9, control=121, value=0, time=0)
      Message('control_change', channel=9, control=10, value=64, time=0)
      Message('control_change', channel=9, control=93, value=0, time=0)
      Message('control_change', channel=9, control=7, value=100, time=0)
      Message('control_change', channel=9, control=91, value=0, time=0)
      MetaMessage('key_signature', key='C', time=0)
      Message('note_on', channel=9, note=36, velocity=100, time=0)
      ...
      Message('note_off', channel=9, note=42, velocity=64, time=0)
      MetaMessage('end_of_track', time=190080)

Here we get 2 Tracks, with more meta messages an control change messages. This is a good example for our own MIDI file.

We see that in the hydrogen file and in the Rosegarden file the messages for tempo and time signature are the same.

But what with the note on and off messages? We see very different delta times for the notes.

pattern
click for a better view

So why we get different delta time tick numbers? The answer resides in the header chunk (first unit) of the MIDI file (see later). In the division field of the chunk the number of ticks per quarter note is specified. For example, if the division field is set to 480, it means there are 480 ticks per quarter note.

In MIDO we can read that information with midi_file.ticks_per_beat (ticks per beat assumes in MIDI files that we use quarter notes; the term is less precise). Let's expand the code:

    from mido import MidiFile

    mfile = MidiFile("simple_drum_pattern_hydrogen_t0.mid")
    print(f"File format: {mfile.type}")
    print(f"Ticks per quarter note: {mfile.ticks_per_beat}")
    for i, track in enumerate(mfile.tracks):
      print(f'=== Track {i}')
      for message in track:
          print(f'  {message!r}')

An now we see the difference:

Hydrogen file Rosegarden file
Ticks per quarter note: 192 480
Ticks per quarter note: dt 48 = 1/4 quarter note = 1/16 note dt 120 = 1/4 quarter note = 1/16 note
dt 240 = 1/2 quarter note = 1/8 note

So here we see that the difference between noteon and noteoff in hydrogen is always 1/16 note and in Rosegarden we got different note length if we use the default settings of the program. For drums this is mostly not relevant because they are short. In DRUMPPY we can adjust the length with the stretch parameter.

How does the delta time works?

In a MIDI track the events (messages), are sequencing events and each MIDI event (such as note on, note off, control change, etc.) is followed by a delta time value. This value indicates how much time should pass before the next event (any type of MIDI event!) occurs . Example: Note on with a delta time of 0, means this note should start immediately. If the next event is a note off for the same note with a delta time of 120 ticks, it means the note should be held for 120 ticks (e.g one 16th note with PPQN = 480) before being turned off.

Example:

    Message('note_on', note=36, velocity=64, time=0)   # The first note_on event (kick) starts immediately (dt = 0).
    Message('note_on', note=42, velocity=64, time=0)   # The second note_on event (hi-hat) starts immediately (dt = 0).
    Message('note_off' note=36, velocity=64, time=120) # note_off for kick (36) occurs 1/16 note (120 ticks) after note_on
    Message('note_off' note=42, velocity=64, time=0)   # note_off for hi-hat (42) occurs 1/16 note after note_on (same as kick dt = 0)
    Message('note_on', note=42, velocity=64, time=120) # next note_on event (hi-hat) starts 1/16 note later (2/16 note after beginning)
    Message('note_off' note=42, velocity=64, time=120) # note_off for hi-hat (42) 1/16 note after note_on

But which timing in BPM our files uses?

The tempo is set to 500000 µs or 0.5 s per quarter note or 2 quarter notes per second. This means we 120 quarter notes per minute or 120 bpm!

Creating an SMF with the Python MIDO library

Now let't try to recreate the simple drum pattern as a MIDI file type 1. Mistral.ai tells us that 480 ticks per quarter note is a quasi standard, so let's stick with that. It's also the default of the MIDO library, but we set it remember that.

    from mido import MidiFile, MidiTrack, Message, MetaMessage, tempo2bpm

    mfile = MidiFile(type=1) # Type = 1
    ppqn = 480
    mfile.ticks_per_beat = ppqn # Set the number of ticks per quarter note
    mtrack0 = MidiTrack() # first track with infos
    mtrack1 = MidiTrack() # second track with notes
    mfile.tracks.append(mtrack0)
    mfile.tracks.append(mtrack1)
    bpm = 120 # 500000 microseconds per quarter at 120bpm
    tempo = int(tempo2bpm(bpm))
    dt_1_16 = int(ppqn/4) # with ppqn = 480 we get 120
    ### first track ###
    mtrack0.append(MetaMessage('copyright', text='CC BY-SA'))
    mtrack0.append(MetaMessage('cue_marker', text='Created by weigu (DRUMMPY)'))
    mtrack0.append(MetaMessage('cue_marker', text='http://www.weigu.lu'))
    mtrack0.append(MetaMessage('set_tempo', tempo=tempo))
    mtrack0.append(MetaMessage('time_signature', numerator=4, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8))
    mtrack1.append(MetaMessage('end_of_track', time=0))
    ### second track ###
    mtrack1.append(MetaMessage('track_name', name='simple_drum_pattern'))
    for i in range(2):
        if i == 0:
            mtrack1.append(Message('note_on', channel=9, note=36, velocity=64, time=0))       # kick on
        else:
            mtrack1.append(Message('note_on', channel=9, note=36, velocity=64, time=dt_1_16)) # kick on
        mtrack1.append(Message('note_on', channel=9, note=42, velocity=64, time=0))           # hi-hat on
        mtrack1.append(Message('note_on', channel=9, note=42, velocity=64, time=0))           # hi-hat on
        mtrack1.append(Message('note_off', channel=9, note=36, velocity=64, time=dt_1_16))    # kick off
        mtrack1.append(Message('note_off', channel=9, note=42, velocity=64, time=0))          # hi-hat off
        mtrack1.append(Message('note_on', channel=9, note=42, velocity=64, time=dt_1_16))     # hi-hat on
        mtrack1.append(Message('note_off', channel=9, note=42, velocity=64, time=dt_1_16))    # hi-hat off
        mtrack1.append(Message('note_on', channel=9, note=38, velocity=64, time=dt_1_16))     # snare on
        mtrack1.append(Message('note_on', channel=9, note=42, velocity=64, time=0))           # hi-hat on
        mtrack1.append(Message('note_off', channel=9, note=38, velocity=64, time=dt_1_16))    # snare off
        mtrack1.append(Message('note_off', channel=9, note=42, velocity=64, time=0))          # hi-hat off
        mtrack1.append(Message('note_on', channel=9, note=42, velocity=64, time=dt_1_16))     # hi-hat on
        mtrack1.append(Message('note_off', channel=9, note=42, velocity=64, time=dt_1_16))    # hi-hat off
    mtrack1.append(MetaMessage('end_of_track', time=dt_1_16)) # length overall = 16/16 notes
    mfile.save('simple_drum_pattern_w_mido.mid')

Analysing an SMF bytewise (2008)

The file

The MIDI file (Lancelot20etudes_10.mid) that I analysed in 2008 was created with Rosegarden. I contains a little Intro (4 beat to get the tempo) and a Clarinet music piece.

midifile
click for a better view

Today it is easier to use the MIDO library with Python to get the main information. Let's do this to be able check if the findings from 2008 are ok:

    File format: 1
    Ticks per quarter note: 480
    === Track 0
      MetaMessage('copyright', text='2007', time=0)
      MetaMessage('cue_marker', text='Created by Rosegarden', time=0)
      MetaMessage('cue_marker', text='http://www.rosegardenmusic.com/', time=0)
      MetaMessage('set_tempo', tempo=500000, time=0)
      MetaMessage('time_signature', numerator=2, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8, time=0)
      MetaMessage('end_of_track', time=0)
    === Track 1
      MetaMessage('track_name', name='Lancelot_20_10', time=0)
      Message('program_change', channel=1, program=71, time=0)
      Message('control_change', channel=1, control=7, value=100, time=0)
      Message('control_change', channel=1, control=10, value=64, time=0)
      MetaMessage('text', text='mf', time=1920)
      MetaMessage('text', text='Allegretto', time=0)
      Message('note_on', channel=1, note=55, velocity=100, time=0)
      ...
      Message('note_off', channel=1, note=79, velocity=127, time=864)
      MetaMessage('end_of_track', time=0)
    === Track 2
      MetaMessage('track_name', name='Intro', time=0)
      Message('program_change', channel=9, program=0, time=0)
      Message('control_change', channel=9, control=7, value=100, time=0)
      Message('control_change', channel=9, control=10, value=64, time=0)
      Message('note_on', channel=9, note=64, velocity=100, time=0)
      ...
      Message('note_off', channel=9, note=64, velocity=127, time=480)
      MetaMessage('end_of_track', time=0)

The file contains three tracks. The first track is mainly used to save text data. The second track contains the piece of music (channel 1). First, however, the clarinet is selected according to the GM standard (program change, program = 71) and the general volume is set to 100 (control = 7), and the aftertouch for Mi (E) is set to 148.

The first unit (header-chunk) MThd

Data in midi files is stored in so-called "chunks" (units). Such a unit consists of a group of bytes with a unique ID number and number of bytes.

The first unit (header-chunk) with the identifier MThd consists of the ASCII characters MThd and 4 bytes to store the length. The length of this unit does not change and is always 6 bytes.

Contents Number of bytes Meaning
MThd 4 ASCII Identifier
0x00 0x00 0x00 0x06 4 Number of following bytes (always 6)
0x00 0x0v 2 v = Format: 0 = type 0, 1 = type1, 2 = type2
0x00 0xvv 2 0xvv = NumTracks: Number of stored tracks (type 0 always 1)
0xvvvv 2 Division: beats per quarter note (PPQN)

All data in Big Endian (MSB first)!

midi SMF header

The following MTrk units (track data)

All of the following units are MIDI Track (identifier MTrk) units (except proprietary units).

An MTrk unit contains all MIDI data with time information for a track. The number of MTrk units was therefore defined in the header (MThd, NumTracks).

The MTrk unit begins with the identifier MTrk followed by 4 bytes, which contain the number of data bytes.

Contents Number of bytes Meaning
MTrk 4 ASCII Identifier
0xvvvvvvvv 4 Number of following bytes (track)
data 2^32 = 4294967296 track data

All data in Big Endian (MSB first)!

Time difference

Track data consists of sequential MIDI events and non-MIDI events (e.g. text). Each MIDI event is assigned to a specific time. The information for the time assignment (time difference) is always saved before the event and refers to the preceding event. A time difference to the previous event is therefore saved (delta-time). Time difference (delta-time) in ticks refers to time base PPQN. The first time difference before the first event (1-4 bytes) is zero. The time difference is stored in several bytes (maximum four), whereby the number of bytes depends on the size of the time difference (variable length quantity). Only 7 data bits are used in each byte. The first bit (MSBit) is 1 as long as further bytes follow else it is 0. The last byte contains a zero in the MSBit (maximum number is 0xFFFFFFF)

Examples:

0x81 0x00 = 0b(1)000 0001 0b(0)000 0000 => 0b1000 0000 = 0x80 = 128
0x88 0x80 0x00 = 0b(1)000 1000 0b(1)000 0000 0b(0)000 0000 => 0b10 0000 0000 0000 0000 = 0x20000 = 131072

MIDI events

A normal MIDI event begins with a status byte. This is followed by 1 or 2 data bytes and this is followed by the time difference.

SYSEX events (SYStem EXclusive events (status 0xF0)) are special events, as they can be of any length. The mandatory 0xF0 byte is followed by up to four bytes (coding with \"variable length quantity\"), which indicate how many bytes will follow. Normally a SYSEX event ends with 0xF7.

Contents Number of bytes Meaning
0x80-0x8F nn ve 2 Note-Off: LowNibble = midi channel; nn: note number (0-127 (semitone steps): c' (do)= 60 (0x3C))
ve = velocity (0-127, velocity with release)
0x90-0x9F nn ve 2 Note-On: LowNibble = midi channel; nn: note number (0-127 (semitone steps): c' (do)= 60 (0x3C))
ve = velocity (0-127, force is played with the note, default 64 0x40, zero 0 can replace Note-Off)
0xA0-0xAF nn pa 2 Aftertouch: LowNibble = midi channel; nn: note number (0-127 (semitone steps): c' (do)= 60 (0x3C))
pa = pressure amount (0-127)
0xB0-0xBF cn va 2 Controller: LowNibble = midi channel; cn: controller number (0-127) va = value for controller (0-127)
0xC0-0xCF pn 1 Program Change: LowNibble = midi channel; pn: program number to change (0-127)
0xD0-0xDF pa 1 Channel Pressure: LowNibble = midi channel; pa: pressure amount (0,127)
0xE0-0xEF v1 v2 2 Pitch Wheel: LowNibble = midi channel; v1 v2: 14-bit value (each without MSB); change in
parts of a semitone (device-dependent); default (0x2000, centre); whole range after GM +/- 2 semitones
0xF0 len data 0xF7 len System Exclusive SYSEX: Data embedded between 0x0F and 0xF7
0xF1 ...
0xF2
Meta events (non-MIDI events)

Meta events are non-MIDI events. 0xFF is reserved for non-MIDI events (0xFF = reserved for RESET for MIDI; makes no sense in a file). A time difference is also granted to the meta events. MIDI and META events can be mixed as desired.

0xFF is followed by a byte that indicates which non-MIDI event is involved (2 status bytes). This is followed by 1-4 bytes (coding with "variable length quantity") that indicate the number of bytes.

Contents Bytes Meaning
0xFF 0x00 0x02 ss ss 2 Sequence Number (optional)
0xFF 0x01 len "text" len Text for comments, preferably at the beginning of the track
0xFF 0x02 len "text" len Copyright text
0xFF 0x03 len "text" len Sequence/Track Name
0xFF 0x04 len "text" len Instrument
0xFF 0x05 len "text" len Lyric (1 syllable)
0xFF 0x06 len "text" len Marker
0xFF 0x07 len "text" len Cue Point (cue points act as markers within a MIDI file)
0xFF 0x08 len "text" len Programme (Patch) Name
0xFF 0x08 len "text" len Programme (Patch) Name
0xFF 0x08 len "text" len Programme (Patch) Name
0xFF 0x08 len "text" len Programme (Patch) Name
0xFF 0x08 len "text" len Programme (Patch) Name
0xFF 0x09 len "text" len Device (Port) Name
0xFF 0x2F 0x00 0 end of track (mandatory!!)
0xFF 0x51 0x03 tt tt tt 3 tempo in microseconds per quarter note (default 120bpm)
0xFF 0x54 0x05 hr mn se fr ff 5 SMPTE Offset (hours, minutes, seconds, frames, subframes)
0xFF 0x58 0x04 zz nn mn bb 4 Time Signature nn: Numerator nn: Denominator as a power of 2 (3: 2³=8)
default: 4/4\ mn: MIDI bars per metronome Click\ bb: Number of 32nd/quarter notes
0xFF 0x59 0x02 sf mi 2 Key Signature sf: 0 = C (do) -x = flats (bémol) +x sharps (dièse) mi: 0 = major (maj.)1 = minor (min.)
0xFF 0x7F len data len Proprietary Event
Time base PPQN (ticks), tempo, clock and SMPTE

The time base (PPQN and ticks) and the tempo are 2 independent variables!

Tempo

The tempo can be set via meta event tempo. Tempo (in microseconds per quarter note) or in music BPM (Beats (quarter notes) Per Minute) are interdependent like period duration and frequency.

BPM = 60000000/tempo => tempo = 60000000/BPM

Examples:

120⁥BPM = 500000⁥µs per quarter note 60⁥BPM = 1000000⁥µs (1⁥s) per quarter note

Time base PPQN (divisor)

PPQN (Pulses Per Quarter Note) or Ticks per quarter note corresponds to an internal hardware timer and is defined by the divisor in the header of the MIDI file.

Length in µs of a pulse (tick) = tempo/divisor

SMPTE

Comes from the film industry (hours, minutes and seconds). A second is divided into frames: 24/25/29/30 frames in subframes. SMPTE can be set with a meta event and is independent of the music speed.

MIDI Clock

May be required to synchronise 2 devices with 24 MIDI clocks per quarter note!

Analysing the file

As seen in the header we have a type 1 file with 3 tracks and a PPQN of 480 ticks per quarter note.

Locking at our bytes we see that for the first track:

The second track contains the piece of music wiz 0x6FE = 1790 Byte.

midi SMF body

Score for clarinet in Sib: everything is played one note lower (transposition given in Rosegarden)

After the meta events we have the the actual song:

delta time in ticks note on / vel delta time in ticks note off / vel
0x00 Sol (G, 0x37) / 100 0x83 0x30 Sol (0x37) / 127
0x30 La# (A#, 0x3A) / 93 0x81 0x20 La# (0x3A) / 127
0x00 La (A, 0x39) / 87 0x81 0x20 La (0x39) / 127
0x30 Re (D, 0x3E) / 87 0x81 0x10 Re (0x3E) / 127
0x10 etc.

In the header: Divisor = 480/quarter note!

0x83 0x30 = 0b110110000 = 432 => 432/480 = 0.9 quarter note
0x30 = 48 => 48/480 = 0.1 quarter note
0x81 0x20 = 0b10100000 = 160 => 160/480 = 1/3 quarter note
0x81 0x10 = 0b10010000 = 144 => 144/480 = 0.9x1/3 quarter note
0x10 = 16 => 16/480 = 0.1x1/3 quarter note

Do/C Do#/C# Re/D Re#/D# Mi/E Fa/F Fa#/F# Sol/G Sol#/G# La/A La#/A# Si/B
0x30/48 0x31/49 0x32/50 0x33/51 0x34/52 0x35/53 0x36/54 0x37/55 0x38/56 0x39/57 0x3A/58 0x3B/59
0x3C/60 0x3D/61 0x3E/62 0x3F/63 0x40/64 0x41/65 0x42/66 0x43/67 0x44/68 0x45/69 0x46/70 0x47/71
0x48/72 0x49/73 0x4A/74 0x4B/75 0x4C/76 0x4D/77 0x4E/78 0x4F/79 0x50/80 0x51/81 0x52/82 0x53/83

Downloads

Interesting links