MIDI (Musical Instrument Digital Interface) is a popular standard for representing music digitally. It allows composers and developers to create, modify, and manipulate music in a digital format. In this article, we will walk through a Python script that can generate a MIDI file using a sequence of notes, lengths, velocities, and even macros for repeated musical patterns.
Overview of the Script
The script provided uses the mido
library, a powerful Python package that allows you to work with MIDI files. The goal of this script is to generate a MIDI file based on a sequence of notes and their properties such as length and velocity. Additionally, it provides the functionality of expanding macros into musical sequences, making it easier to reuse patterns.
Let’s break down the key components of the script and explain how everything works step by step.
1. Setting Up the Environment
First, we import the necessary modules. The mido
library is used for handling MIDI file creation, message sending, and setting tempo. The os
module ensures that the output folder exists before we save the generated file.
import mido
from mido import MidiFile, MidiTrack, Message
import os
2. Mapping Notes to MIDI Numbers
In MIDI, each note corresponds to a unique number. For example, the note 'C4' is assigned to the number 60. The script uses a dictionary NOTE_TO_MIDI
to map note names (like 'C', 'D#', etc.) to MIDI note numbers.
NOTE_TO_MIDI = {
'C': 0, 'C#': 1, 'D': 2, 'D#': 3, 'E': 4, 'F': 5, 'F#': 6, 'G': 7, 'G#': 8, 'A': 9, 'A#': 10, 'B': 11
}
3. Converting Notes and Octaves to MIDI Numbers
The note_to_midi
function takes a note (like 'C4' or 'D#3') and calculates the corresponding MIDI number. The formula adjusts for the octave as well, ensuring that C4 is always MIDI number 60.
def note_to_midi(note):
note_name = note[:-1] # Extract the note name (e.g., 'A', 'B', etc.)
octave = int(note[-1]) # Extract the octave (e.g., '0', '2', etc.)
return NOTE_TO_MIDI[note_name] + 12 * (octave + 1)
4. Converting Length to MIDI Ticks
MIDI time is divided into ticks, which represent small time intervals. The length_to_ticks
function converts musical note lengths (like whole, half, quarter, etc.) to their corresponding duration in MIDI ticks, based on the ticks per beat value.
def length_to_ticks(length, ticks_per_beat):
if length == 1:
return ticks_per_beat * 4 # Whole note
elif length == 2:
return ticks_per_beat * 2 # Half note
elif length == 4:
return ticks_per_beat # Quarter note
elif length == 8:
return ticks_per_beat // 2 # Eighth note
elif length == 16:
return ticks_per_beat // 4 # Sixteenth note
return ticks_per_beat # Default to quarter note
5. Generating the MIDI File
The generate_piano_midi
function creates a new MIDI file by first setting up a track and defining the instrument (a piano by default, but this can be changed). It then processes the provided sequence of notes, lengths, and velocities, adding the appropriate MIDI messages (note on and note off) to the track.
def generate_piano_midi(sequence, tempo, output_file="output/piano_piece_2.mid", instrument_program=0):
output_folder = "output"
if not os.path.exists(output_folder):
os.makedirs(output_folder)
midi = MidiFile()
track = MidiTrack()
midi.tracks.append(track)
track.append(Message('program_change', program=instrument_program))
microseconds_per_beat = 60 * 1000000 // tempo
track.append(mido.MetaMessage('set_tempo', tempo=microseconds_per_beat))
ticks_per_beat = 480
current_time = 0 # Start at time 0
for i in range(0, len(sequence), 3):
note = sequence[i] # Note name (e.g., 'A0')
length = sequence[i + 1] # Length (e.g., 4 for quarter note)
velocity = sequence[i + 2] # Velocity (e.g., 100 for volume)
if note == 'X': # Handle pauses (no sound)
track.append(Message('note_on', note=0, velocity=0, time=current_time))
track.append(Message('note_off', note=0, velocity=0, time=length_to_ticks(length, ticks_per_beat)))
current_time = 0
continue
midi_note = note_to_midi(note)
duration = length_to_ticks(length, ticks_per_beat)
track.append(Message('note_on', note=midi_note, velocity=velocity, time=current_time))
track.append(Message('note_off', note=midi_note, velocity=velocity, time=duration))
current_time = 0
midi.save(output_file)
print(f"MIDI file '{output_file}' has been saved!")
6. Expanding Macros
A powerful feature of this script is its ability to expand macros. Macros are predefined sequences of notes that can be referenced by a shorthand notation. This allows you to easily reuse patterns throughout your musical sequence without repeating the same code.
def expand_macros(sequence, macros):
expanded_sequence = []
i = 0
while i < len(sequence):
if isinstance(sequence[i], str) and sequence[i].startswith('M'): # Macro starts with 'M'
macro_name = sequence[i]
if macro_name in macros:
expanded_sequence.extend(macros[macro_name]) # Expand the macro
i += 1 # Skip the macro name
else:
expanded_sequence.append(sequence[i])
i += 1
return expanded_sequence
7. Creating a Musical Sequence
You can define a musical sequence using a combination of macros and individual notes. For example:
macros = {
'M001': ['C3', 8, 90, 'E3', 8, 85, 'G3', 8, 80, 'C4', 8, 75],
'M002': ['C3', 8, 90, 'F3', 8, 85, 'A3', 8, 80, 'C4', 8, 75],
'M003': ['B2', 8, 90, 'D3', 8, 85, 'G3', 8, 80, 'B3', 8, 75]
}
sequence = ['M001', 'M002', 'M003', 'M003', 'M001']
expanded_sequence = expand_macros(sequence, macros)
8. Final Steps
Once the sequence is expanded, you can set a tempo (in beats per minute), select an instrument (e.g., piano or flute), and generate the MIDI file by calling the generate_piano_midi
function:
tempo_bpm = 120 # Tempo in beats per minute
instrument_program = 1 # Flute
generate_piano_midi(expanded_sequence, tempo_bpm, instrument_program=instrument_program)
Conclusion
This script provides a solid foundation for generating MIDI files in Python. By using the mido
library and implementing features like note-to-MIDI conversion, macro expansion, and time duration calculation, you can easily automate the creation of musical compositions. Whether you're working on a music project, creating soundtracks, or just experimenting with MIDI, this script can save you time and open up creative possibilities.
If you haven’t yet, give it a try and create your own MIDI compositions with Python!
Complete Code
import mido
from mido import MidiFile, MidiTrack, Message
import os
# Mapping of note names to MIDI numbers
NOTE_TO_MIDI = {
'C': 0, 'C#': 1, 'D': 2, 'D#': 3, 'E': 4, 'F': 5, 'F#': 6, 'G': 7, 'G#': 8, 'A': 9, 'A#': 10, 'B': 11
}
# Function to convert note and octave to MIDI number
def note_to_midi(note):
note_name = note[:-1] # Extract the note name (e.g., 'A', 'B', etc.)
octave = int(note[-1]) # Extract the octave (e.g., '0', '2', etc.)
return NOTE_TO_MIDI[note_name] + 12 * (octave + 1)
# Function to convert length notation to MIDI ticks
def length_to_ticks(length, ticks_per_beat):
if length == 1:
return ticks_per_beat * 4 # Whole note
elif length == 2:
return ticks_per_beat * 2 # Half note
elif length == 4:
return ticks_per_beat # Quarter note
elif length == 8:
return ticks_per_beat // 2 # Eighth note
elif length == 16:
return ticks_per_beat // 4 # Sixteenth note
return ticks_per_beat # Default to quarter note
# Function to generate MIDI file based on the given sequence and tempo
def generate_piano_midi(sequence, tempo, output_file="output/piano_piece_2.mid", instrument_program=0):
output_folder = "output"
if not os.path.exists(output_folder):
os.makedirs(output_folder)
midi = MidiFile()
track = MidiTrack()
midi.tracks.append(track)
track.append(Message('program_change', program=instrument_program))
microseconds_per_beat = 60 * 1000000 // tempo
track.append(mido.MetaMessage('set_tempo', tempo=microseconds_per_beat))
ticks_per_beat = 480
current_time = 0
for i in range(0, len(sequence), 3):
note = sequence[i] # Note name (e.g., 'A0')
length = sequence[i + 1] # Length (e.g., 4 for quarter note)
velocity = sequence[i + 2] # Velocity (e.g., 100 for volume)
if note == 'X': # Handle pauses (no sound)
track.append(Message('note_on', note=0, velocity=0, time=current_time))
track.append(Message('note_off', note=0, velocity=0, time=length_to_ticks(length, ticks_per_beat)))
current_time = 0
continue
midi_note = note_to_midi(note)
duration = length_to_ticks(length, ticks_per_beat)
track.append(Message('note_on', note=midi_note, velocity=velocity, time=current_time))
track.append(Message('note_off', note=midi_note, velocity=velocity, time=duration))
current_time = 0
midi.save(output_file)
print(f"MIDI file '{output_file}' has been saved!")
# Function to expand macros
def expand_macros(sequence, macros):
expanded_sequence = []
i = 0
while i < len(sequence):
if isinstance(sequence[i], str) and sequence[i].startswith('M'): # Macro starts with 'M'
macro_name = sequence[i]
if macro_name in macros:
expanded_sequence.extend(macros[macro_name]) # Expand the macro
i += 1 # Skip the macro name
else:
expanded_sequence.append(sequence[i])
i += 1
return expanded_sequence
# Example macros definition
macros = {
'M001': ['C3', 8, 90, 'E3', 8, 85, 'G3', 8, 80, 'C4', 8, 75],
'M002': ['C3', 8, 90, 'F3', 8, 85, 'A3', 8, 80, 'C4', 8, 75],
'M003': ['B2', 8, 90, 'D3', 8, 85, 'G3', 8, 80, 'B3', 8, 75],
'M004': ['C3', 8, 90, 'E3', 8, 85, 'G3', 8, 80, 'C4', 8, 75],
'M005': ['C3', 8, 90, 'E3', 8, 85, 'A3', 8, 80, 'C4', 8, 75],
'M006': ['D3', 8, 90, 'F#3', 8, 85, 'A3', 8, 80, 'C4', 8, 75],
'M007': ['G2', 8, 90, 'B2', 8, 85, 'D3', 8, 80, 'G3', 8, 75],
'M008': [ 'C3', 1, 90 ]
}
# Example sequence
sequence = [
'M001',
'M001',
'M002',
'M002',
'M003',
'M003',
'M004',
'M004',
'M005',
'M005',
'M006',
'M006',
'M007',
'M007',
'M008'
]
# Expand macros in the sequence
expanded_sequence = expand_macros(sequence, macros)
# Tempo in beats per minute (e.g., 120 BPM)
tempo_bpm = 120
# Choose an instrument (e.g., piano, flute, violin, etc.)
instrument_program = 1 # Flute
# Generate the MIDI file with the expanded sequence and tempo
generate_piano_midi(expanded_sequence, tempo_bpm, instrument_program=instrument_program)
Top comments (0)