Reading MIDI files

Hi all! Someone sent me a message the other day asking about importing data from MIDI files, and I thought I’d post the example here, in case others are interested! It uses the mido library. The code below creates a list of channels, pitches, volumes, start_times, lengths, and inter-onset times, and then uses them to play back the file with SCAMP. But you can do anything with these lists! For example, you could use random.shuffle to shuffle all the pitches and durations.

Anyway, if you make something fun with this, share it!

from mido import MidiFile
from scamp import *
from collections import namedtuple

INPUT_FILE_PATH = 'bwv772.mid'

# -------------------------------- Processing the MIDI Data ---------------------------------------

# following https://www.twilio.com/blog/working-with-midi-data-in-python-using-mido
# MIDO Documentation (a little confusing): https://mido.readthedocs.io/en/latest/index.html

mid = MidiFile(INPUT_FILE_PATH, clip=True)

# print(mid)
#
# for track in mid.tracks:
#     print(track)
#
# for message in mid.tracks[1]:
#     print(message)

Note = namedtuple("Note", "channel pitch volume start_time length")

notes_started = {}
notes = []

for track in mid.tracks:
    t = 0
    for message in track:
        t += message.time/mid.ticks_per_beat
        if message.type == "note_off" or (message.type == "note_on" and message.velocity == 0):
            volume, start_time = notes_started[(message.note, message.channel)]
            notes.append(Note(message.channel, message.note, volume, start_time, t - start_time))
        elif message.type == "note_on":
            notes_started[(message.note, message.channel)] = message.velocity / 127, t

notes.sort(key=lambda note: note.start_time)

channels, pitches, volumes, start_times, lengths = zip(*notes)
channels = list(channels)
pitches = list(pitches)
volumes = list(volumes)
start_times = list(start_times)
lengths = list(lengths)

inter_onset_times = [t2 - t1 for t1, t2 in zip(start_times[:-1], start_times[1:])]

# -------------------------------- Doing something interesting with it! ---------------------------------------

# The code below just plays back the music as-is, but what can you do with it?

s = Session()

piano = s.new_part("piano")

for wait_time, pitch, length, volume in zip(inter_onset_times, pitches, lengths, volumes):
    piano.play_note(pitch, volume, length, blocking=False)
    wait(wait_time)
3 Likes

Hi Marc, I’ve tried using random.shuffle to create something interesting, but to little avail. Can you help me?

from mido import MidiFile
from scamp import *
from collections import namedtuple
import random

INPUT_FILE_PATH = 'Canon.mid'

# -------------------------------- Processing the MIDI Data ---------------------------------------

mid = MidiFile(INPUT_FILE_PATH, clip=True)

# print(mid)

# for track in mid.tracks:
#       print(track)

#  for message in mid.tracks[1]:
#      print(message)

Note = namedtuple("Note", "channel pitch volume start_time length")

notes_started = {}
notes = []

for track in mid.tracks:
    t = 0
    for message in track:
        t += message.time/mid.ticks_per_beat
        if message.type == "note_off" or (message.type == "note_on" and message.velocity == 0):
            volume, start_time = notes_started[(message.note, message.channel)]
            notes.append(Note(message.channel, message.note, volume, start_time, t - start_time))
        elif message.type == "note_on":
            notes_started[(message.note, message.channel)] = message.velocity / 127, t

notes.sort(key=lambda note: note.start_time)

channels, pitches, volumes, start_times, lengths = zip(*notes)
channels = list(channels)
pitches = list(pitches)
volumes = list(volumes)
start_times = list(start_times)
lengths = list(lengths)

inter_onset_times = [t2 - t1 for t1, t2 in zip(start_times[:-1], start_times[1:])]

# -------------------------------- Doing something interesting with it! ---------------------------------------

# The code below just plays back the music as-is, but what can you do with it?

s = Session()

clarinetto = s.new_part("clarinetto")

s.start_transcribing()

for wait_time, pitch, length, volume in zip(inter_onset_times, pitches, lengths, volumes):
    clarinetto.play_note(random.shuffle (pitches), random.shuffle (volumes), random.shuffle(lengths), blocking=False)
    wait(wait_time)
    
performance = s.stop_transcribing()

performance.to_score().show()

You think that random.shuffle(pitches) returns a new list with the pitches shuffled. But it doesn’t. It actually returns None. The “pitches” list itself is shuffled.

1 Like

Such an easy and common mistake to make!

1 Like

Thank you so much for answering me!
How would you solve my situation?
The idea would be to use the various parameters of the MIDI file to create a new melody, in a new MIDI/xml/pdf file, which derives from the Original MIDI file, obviously changing the pitch, volume and duration of the notes.
Could you help me?

Thank you so much for answering me!
How would you solve my situation?
The idea would be to use the various parameters of the MIDI file to create a new melody, in a new MIDI/xml/pdf file, which derives from the Original MIDI file, obviously changing the pitch, volume and duration of the notes.
Could you help me?

Do the random.shuffle statements before playing the note:

The result is not guaranteed though.

Hi Scampsters !
The code proposed by Marc in january 2022 fails to process a midi file produced by the code below. Is it because of the “Blocking = False” option ?

The code below produces a score in pdf, a “.wav” and a “.mid” files.
If the midi file is played with the same soundfont, should not the wav and mid files sound perfectly similar ? …

from scamp import *
outpu=r"C:\Users\Admin\Documents\scamp\export\my_first_score"
t1=".wav"
t2=".mid"
playback_settings.recording_file_path = outpu+t1 # .wav output 

s = Session()
s.tempo=100
my_piano=s.new_part(name = "piano") 


L3 = NoteProperties([StaffText("3", placement="below")])
L2 = NoteProperties([StaffText("2", placement="below")])
L1 = NoteProperties([StaffText("1", placement="below")])
def My_first_test(nbre_mesures):
    for _ in range(nbre_mesures):
        my_piano.play_note(36, 1, 1,[L3,"articulation: accent"],blocking=False)
        my_piano.play_note(36, 0, 1/4)
        my_piano.play_note(52, 0.6, 1/4)
        my_piano.play_note(55, 0.5, 1/4)
        my_piano.play_note(59, 0.4, 1/4)
        my_piano.play_note(40, 0.4, 1,L1,blocking=False)
        my_piano.play_note(40, 0, 1/4)
        my_piano.play_note(56, 0.4, 1/4)
        my_piano.play_note(59, 0.3, 1/4)
        my_piano.play_note(62, 0.2, 1/4)
        my_piano.play_note(33, 0.4, 1,L3,blocking=False)
        my_piano.play_note(33, 0, 1/4)
        my_piano.play_note(64, 0.3, 1/4)
        my_piano.play_note(40, 0.2, 1/4)
        my_piano.play_note(40, 0.1, 1/4)
        my_piano.play_note(38, .3, 1,L1,blocking=False)
        my_piano.play_note(38, 0, 1/4)
        my_piano.play_note(36, 0.2, 1/4,L2)
        my_piano.play_note(36, 0.1, 1/4,L3)
        my_piano.play_note(36, 0.15, 1/4,L2)        


s.start_transcribing()
s.fork(My_first_test, args=[2])
s.wait_for_children_to_finish()
performance=s.stop_transcribing()
partition=performance.to_score(title=" Test 01",composer="Decomposer & Co",time_signature="4/4")
partition.show() # score in pdf
#partition.show_xml()
performance.export_to_midi_file(outpu+t2) # .mid output

The issue is that sometimes people use a note on event with a velocity of 0 instead of a note off event to signal the end of a note. So my midi reading script has the line:

if message.type == "note_off" or (message.type == "note_on" and message.velocity == 0):
    etc...

But your script that generates the MIDI file uses some notes with volume zero. That causes my reading script to think it’s ending a note when it’s starting a note.

You can just remove the or (message.type == "note_on" and message.velocity == 0) and it works.

Clearly explained. Thanks a lot Marc !

1 Like

Just an update on this thread. I’ve incorporated a little midi parsing utility into scamp_extensions:

from scamp import *
from scamp_extensions.parsing.midi import scrape_midi_file_to_dict


midi_data = scrape_midi_file_to_dict('vp3-1pre.mid')

print(midi_data)


s = Session()
s.tempo = 110

piano = s.new_part("piano")

for wait_time, pitch, length, volume in zip(midi_data['inter_onset_times'],
                                            midi_data['pitches'],
                                            midi_data['lengths'],
                                            midi_data['volumes']):
    piano.play_note(pitch, volume, length, blocking=False)
    wait(wait_time)

It’s a little limited in that it pools together all of the midi from all the tracks into one dictionary, but I suppose you could just edit the midi files if you wanted one particular track.