MIDIUtil

repository·develop·Indexed 42 days ago

https://github.com/markcwirt/midiutil

A pure Python library for writing multi-track MIDI files in format 1 and format 2. It provides an object-oriented interface via the MIDIFile class to add notes, tempo changes, time signatures, key signatures, and MIDI control events such as program changes, pitch wheel movements, and System Exclusive (SysEx) messages.

Tokens
8.5K
Snippets
33
Records
35
Agent score
54%

What's inside MIDIUtil

  1. Understand the difference between MIDI file formats 1 and 2

    develop

    The file_format parameter determines how tracks and tempo/time signature events are handled:

    Format 1 (Default)

    • Uses a separate, internal "tempo" track for tempo and time signature events.
    • The track parameter in addTempo() and addTimeSignature() is ignored.
    • Track indexing is zero-based for musical tracks. Because the tempo track is managed independently, you can use high track indices for tempo events without creating those tracks (e.g., calling addTempo(1000, 0, 120) is valid even if numTracks=1).

    Format 2

    • Tempo and time signature events are treated as literal events on the specified track.
    • Track indexing is zero-based and strictly follows the numTracks count.
    # Format 1 example: tempo track is managed independently
    MyMIDI = MIDIFile(1, file_format=1)
    track = 0
    big_track = 1000
    MyMIDI.addTempo(big_track, 0, 120) # This works even though track 1000 doesn't exist
    MyMIDI.addNote(track, 0, 69, 0, 1, 100)
  2. How deinterleaving handles overlapping notes

    develop

    The MIDI standard uses note on and note off events rather than continuous "notes". When notes overlap (e.g., a second note starts before the first one ends), it can create ambiguity for instruments.

    If deinterleave=True, the library automatically shifts the note off event of the first note to occur immediately before the note on event of the second note. This ensures a clear sequence of events.

    Example of deinterleaving logic: If you add two notes:

    1. time=0, duration=1 (Off event at 1.0)
    2. time=0.5, duration=1 (Off event at 1.5)

    With deinterleave=True, the events become:

    • Note 1 On: 0
    • Note 1 Off: 0.5 (shifted from 1.0)
    • Note 2 On: 0.5 (processed after the note off)
    • Note 2 Off: 1.5
    # Example of creating overlapping notes that trigger deinterleaving
    time = 0
    duration = 1
    MyMIDI.addNote(track, channel, pitch, time, duration, volume)
    
    time = 0.5
    MyMIDI.addNote(track, channel, pitch, time, duration, volume)
  3. Create a new MIDI event type

    develop

    To add a new MIDI event type, you must create a new subclass of GenericEvent from the MIDIFile module. This subclass is responsible for initializing instance data, defining serialization to a byte stream, and establishing sort order.

    Implementation Requirements:

    1. Class Attributes:
      • evtname: A string identifier for the event.
      • sec_sort_order: An integer used to order events that occur at the same time. Lower numbers appear earlier in the stream.
    2. __init__(self, tick, ..., insertion_order=0):
      • Must accept tick (time in MIDI ticks) and insertion_order.
      • Must call super().__init__(tick, insertion_order).
    3. __eq__(self, other):
      • Define equality logic (e.g., comparing evtname, tick, and specific instance data) to allow the library to identify and remove duplicate events.
    4. __hash__(self):
      • Must explicitly inherit or define __hash__ from GenericEvent (required for Python 3 compatibility).
    5. serialize(self, previous_event_tick):
      • Must return a bytes object representing the MIDI data.
      • Time Handling: All events must begin with a time value calculated using the writeVarLength utility function: writeVarLength(self.tick - previous_event_tick).
      • Structure: Typically follows the pattern: [Variable Length Time] + [Code] + [Subcode] + [Data Length] + [Data].
    class Tempo(GenericEvent):
        '''
        A class that encapsulates a tempo meta-event
        '''
        evtname = 'Tempo'
        sec_sort_order = 3
    
        def __init__(self, tick, tempo, insertion_order=0):
            self.tempo = int(60000000 / tempo)
            super(Tempo, self).__init__(tick, insertion_order)
    
        def __eq__(self, other):
            return (self.evtname == other.evtname and
                    self.tick == other.tick and
                    self.tempo == other.tempo)
    
        __hash__ = GenericEvent.__hash__
    
        def serialize(self, previous_event_tick):
            midibytes = b""
            code = 0xFF
            subcode = 0x51
            fourbite = struct.pack('>L', self.tempo)  # big-endian uint32
            threebite = fourbite[1:4]  # Just discard the MSB
            varTime = writeVarLength(self.tick - previous_event_tick)
            for timeByte in varTime:
                midibytes += struct.pack('>B', timeByte)
            midibytes += struct.pack('>B', code)
            midibytes += struct.pack('>B', subcode)
            midibytes += struct.pack('>B', 0x03)  # length in bytes of 24-bit tempo
            midibytes += threebite
            return midibytes
  4. Quick Start: Create a MIDI file with a C-Major scale

    develop

    To create a MIDI file using MIDIUtil, follow these steps:

    1. Import MIDIFile from midiutil.
    2. Instantiate a MIDIFile object (specifying the number of tracks).
    3. Add events like tempo changes using addTempo().
    4. Add notes using addNote().
    5. Write the file to disk using writeFile().

    Note: When creating a MIDIFile(1), it defaults to MIDI format 1 and automatically creates a tempo track.

    #!/usr/bin/env python
    
    from midiutil import MIDIFile
    
    degrees  = [60, 62, 64, 65, 67, 69, 71, 72]  # MIDI note number
    track    = 0
    channel  = 0
    time     = 0    # In beats
    duration = 1    # In beats
    tempo    = 60   # In BPM
    volume   = 100  # 0-127, as per the MIDI standard
    
    MyMIDI = MIDIFile(1)  # One track, defaults to format 1 (tempo track is created
                          # automatically)
    MyMIDI.addTempo(track, time, tempo)
    
    for i, pitch in enumerate(degrees):
        MyMIDI.addNote(track, channel, pitch, time + i, duration, volume)
    
    with open("major-scale.mid", "wb") as output_file:
        MyMIDI.writeFile(output_file)
  5. Create accessor functions for new events

    develop

    Once a new event class is defined, you must add accessor functions at two levels to make it usable: MIDITrack and MIDIFile.

    1. Add to MIDITrack

    Add a method to the MIDITrack class that instantiates your new event and appends it to self.eventList. This is often boilerplate code.

    def addTempo(self, tick, tempo, insertion_order=0):
        self.eventList.append(Tempo(tick, tempo, insertion_order=insertion_order))

    2. Add to MIDIFile (Public API)

    Add a method to the MIDIFile class. This is the method end-users will actually call. It must handle track selection, time conversion, and insertion ordering.

    Key responsibilities for the MIDIFile accessor:

    • Format Handling: For MIDI Format 1 files, certain events (like Tempo) must be written to track 0, regardless of the requested track.
    • Time Conversion: Public methods should accept time in quarter-notes (beats) by default. Use self.time_to_ticks(time) to convert this to MIDI ticks before passing it to the track method. (Note: If eventtime_is_ticks=True was used during instantiation, time_to_ticks acts as an identity function).
    • Ordering: Use self.event_counter as the insertion_order and increment it after the call to ensure correct event sequencing.
    # Example MIDITrack accessor
    def addTempo(self, tick, tempo, insertion_order=0):
        '''
        Add a tempo change (or set) event.
        '''
        self.eventList.append(Tempo(tick, tempo, 
                              insertion_order=insertion_order))
    
    # Example MIDIFile public accessor
    def addTempo(self, track, time, tempo):
        """
        Add notes to the MIDIFile object
    
        :param track: The track to which the tempo event is added. Note that
            in a format 1 file this parameter is ignored and the tempo is
            written to the tempo track
        :param time: The time (in beats) at which tempo event is placed
        :param tempo: The tempo, in Beats per Minute. [Integer]
        """
        if self.header.numeric_format == 1:
            track = 0
        self.tracks[track].addTempo(self.time_to_ticks(time), tempo, 
                                     insertion_order=self.event_counter)
        self.event_counter += 1
  6. Install MIDIUtil from source

    develop

    You can install MIDIUtil from source by cloning the repository and running the setup script, or by pointing your $PYTHONPATH to the directory containing midiutil (e.g., the src directory).

    To clone via HTTPS:

    git clone https://github.com/MarkCWirt/MIDIUtil.git

    To install from the cloned directory:

    python setup.py install
    git clone https://github.com/MarkCWirt/MIDIUtil.git
    python setup.py install
  7. Quick Start: Create a MIDI file

    develop

    To create a MIDI file with MIDIUtil, follow these steps:

    1. Import MIDIFile from midiutil.
    2. Instantiate a MIDIFile object (specifying the number of tracks).
    3. Add events such as tempo changes and notes using methods like addTempo and addNote.
    4. Write the object to a file using writeFile.

    Note: When creating a MIDIFile(1), it defaults to MIDI Format 1 and automatically creates a tempo track.

    #!/usr/bin/env python
    
    from midiutil import MIDIFile
    
    degrees  = [60, 62, 64, 65, 67, 69, 71, 72] # MIDI note number
    track    = 0
    channel  = 0
    time     = 0   # In beats
    duration = 1   # In beats
    tempo    = 60  # In BPM
    volume   = 100 # 0-127, as per the MIDI standard
    
    MyMIDI = MIDIFile(1) # One track, defaults to format 1 (tempo track 
                         # automatically created)
    MyMIDI.addTempo(track,time, tempo)
    
    for pitch in degrees:
        MyMIDI.addNote(track, channel, pitch, time, duration, volume)
        time = time + 1
    
    with open("major-scale.mid", "wb") as output_file:
        MyMIDI.writeFile(output_file)
  8. Write the MIDI file to disk

    develop

    To save the MIDI data, you must first close() the file to process the tracks and sort events, then use writeFile().

    1. close(): Closes the file for further writing. It sorts MIDI events by time and then by a secondary ordinality (e.g., ensuring Note Offs come before Note Ons at the same timestamp) and adjusts the time origin.
    2. writeFile(fileHandle): Writes the header and all tracks to the provided binary file handle.

    Example:

    with open('output.mid', 'wb') as f:
        midi_file.close()
        midi_file.writeFile(f)
  9. Finalize and write a MIDITrack

    develop

    Before writing a track to a file, you must call closeTrack(). This method performs several critical cleanup steps:

    1. Removes duplicate events if removeDuplicates was set to True during initialization.
    2. Processes the event list to ensure chronological order.
    3. Deinterleaves notes (if deinterleave was set to True) to ensure NoteOn and NoteOff pairs are correctly ordered.

    After closing, use writeTrack(fileHandle) to write the track data to a file-like object.

    # 1. Add your events
    track.addNoteByNumber(0, 60, 0, 480, 100)
    
    # 2. Close the track to process and sort events
    track.closeTrack()
    
    # 3. Write to a file
    with open('output.mid', 'wb') as f:
        track.writeTrack(f)
  10. Example: Implementing non-standard tuning

    develop

    This example demonstrates how to change note tuning (e.g., to avoid a standard 440 Hz A) and ensure the instrument recognizes the new tuning by assigning the program and bank.

    track   = 0
    channel = 0
    tuning  = [(69, 500)]
    program = 0
    bank    = 0
    time    = 0
    
    # Apply the tuning changes
    MyMIDI.changeNoteTuning(track, tuning, tuningProgram=program)
    
    # Explicitly assign bank and program (required by some synthesizers like fluidsynth)
    MyMIDI.changeTuningBank(track, channel, time, bank) 
    MyMIDI.changeTuningProgram(track, channel, time, program)
  11. Change note tuning with changeNoteTuning

    develop

    To implement microtonalities or non-standard tunings, use the changeNoteTuning method. This implements the MIDI tuning standard for real-time note tuning changes.

    Note: Support for the MIDI tuning standard can be inconsistent across different hardware and software synthesizers. Verify compatibility before use.

    # Note: The exact signature is provided by the MIDIFile class
    MyMIDI.changeNoteTuning(track, tuning, tuningProgram=program)