Last updated: 2025-07-21
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.
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.
There are 3 types of MIDI files:
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!
Ticks (PPQN):
Ticks are the smallest unit of time in a MIDI file. They are used to define the timing of musical events. The number of ticks per quarter note (also known as PPQN, or parts per quarter note) is defined in the MIDI file header. Common values are 480, 960, or higher, depending on the resolution needed.
For example, if a MIDI file has a resolution of 480 ticks per quarter note, a quarter note will last 480 ticks, an eighth note will last 240 ticks, and so on. Length in µs of a pulse (tick) = tempo/divisor.
tempo
(µs/quarter note):
Tempo refers to the speed at which a piece of music is played. It is usually measured in beats per minute or µs/quarter note (tempo
in meta event). In a MIDI file, tempo
is specified using a meta-event that indicates the number of microseconds per quarter note.
The default is often 0.5s/quarter note (500000)µs/qn).
Tempo in BPM
:BPM
is used in music. tempo
and BPM
(Beats (quarter notes) Per Minute) are interdependent like period duration and frequency.
BPM = 60000000/tempo => tempo = 60000000/BPM.
Examples:
120BPM = 500000µs per quarter note
60BPM = 1000000µs (1s) per quarter note
Time Signature:
The time signature defines the meter of the music, indicating how many beats are in each measure and which note value constitutes one beat. It is notated as a fraction, such as 4/4 (four quarter notes per measure) or 3/4 (three quarter notes per measure). In MIDI files, the time signature is specified using a meta-event.
Clock (Clocks Per Click):
The MIDI clock is a timing signal used to synchronize devices that are playing or recording MIDI data. It sends out 24 pulses (or clocks) per quarter note (metronome click). This helps ensure that all devices in a MIDI setup stay in time with each other.
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.
Notated 32nd Notes Per Beat:
This refers to how many 32nd notes fit into a single beat. It is a way to describe the subdivision of a beat. For example, in 4/4 time, if the beat is a quarter note, there would be 8 32nd notes per beat (since a 32nd note is 1/8 the duration of a quarter note).
I created 3 MIDI files with a basic rock pop drum pattern (Downloads).
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.
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.
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
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!
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')
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.
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.
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)!
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)!
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
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 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 |
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:
120BPM = 500000µs per quarter note 60BPM = 1000000µs (1s) 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!
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.
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 |