miditoolkit

repository·master·Indexed 43 days ago

https://github.com/yatingmusic/miditoolkit

A Python package for high-performance symbolic music processing and MIDI data file manipulation. It focuses on native 'ticks' time units for MIDI parsing, piano-roll conversion, and chromagram generation. Key features include the MidiFile class for loading and creating MIDI files, tools for pitch padding and resampling, and support for MIDI events such as ControlChange, PitchBend, and TempoChange.

Tokens
3.4K
Snippets
13
Records
15
Agent score
55%

What's inside miditoolkit

  1. Core features of miditoolkit

    master

    Miditoolkit provides several capabilities for MIDI processing:

    MIDI Handling

    • Global Metadata: Access ticks per beat, tempo changes, key signatures, time signatures, lyrics, and markers.
    • Instruments: Manage control changes and pitch bend.
    • Editing: Support for chunk/cropping.
    • IO: Support for BytesIO.

    Piano-rolls

    • Conversion: Tools to convert notes to piano-rolls and vice versa.
    • Analysis: Chromagram generation.
    • Visualization: Tools for visualizing piano-rolls.

    External Integration

    • Supports structure analysis via sf_segmenter.
  2. Understand the tick-based time model in miditoolkit

    master

    Unlike pretty_midi, which operates in seconds, miditoolkit handles MIDI events in ticks, the native time unit of the MIDI protocol.

    When to use miditoolkit vs pretty_midi:

    • Use miditoolkit if you are working with symbolic music or MIDI-specific data where performance and native MIDI timing (ticks) are priorities.
    • Use pretty_midi if your workflow requires time units in seconds (e.g., for music transcription or audio-related tasks).
  3. Create a new MIDI file from scratch

    master

    To programmatically create a MIDI file, follow these steps:

    1. Initialize an empty MidiFile object using mid_parser.MidiFile().
    2. Create Instrument objects using miditoolkit.midi.containers.Instrument and assign them to the instruments list of the MidiFile object.
    3. Create Note objects using miditoolkit.midi.containers.Note with start, end, pitch, and velocity parameters.
    4. Append notes to the notes list of an Instrument.
    5. (Optional) Create Marker objects using miditoolkit.midi.containers.Marker and append them to the MidiFile.markers list.
    6. Use dump(filename) to write the file to disk.
    import numpy as np
    from miditoolkit.midi import parser as mid_parser
    from miditoolkit.midi import containers as ct
    
    # 1. Create empty file
    mido_obj = mid_parser.MidiFile()
    beat_resol = mido_obj.ticks_per_beat
    
    # 2. Create an instrument
    track = ct.Instrument(program=0, is_drum=False, name='example track')
    mido_obj.instruments = [track]
    
    # 3. Create notes
    duration = int(beat_resol * 0.5)
    prev_end = 0
    pitch = 60
    for i in range(10):
        start = prev_end
        end = prev_end + duration
        velocity = np.random.randint(1, 127)
        note = ct.Note(start=start, end=end, pitch=pitch, velocity=velocity)
        mido_obj.instruments[0].notes.append(note)
        
        prev_end = end
        pitch += 1
    
    # 4. Create markers
    marker_hi = ct.Marker(time=0, text='HI')
    mido_obj.markers.append(marker_hi)
    
    # 5. Write to file
    mido_obj.dump('result.midi')
  4. Load and inspect a MIDI file with MidiFile

    master

    Use the MidiFile class to load a MIDI file. Once loaded, you can inspect global properties such as ticks per beat, tempo changes, time signatures, key signatures, markers, and instrument counts.

    from miditoolkit import MidiFile
    from miditoolkit.midi.utils import example_midi_file
    
    path_midi = example_midi_file()
    midi_obj = MidiFile(path_midi)
    
    print(midi_obj)
  5. Crop and pad the pitch axis of a piano-roll

    master

    You can manipulate the pitch dimension of a piano-roll using standard slicing for cropping, or use miditoolkit.pianoroll.utils.pitch_padding to ensure the piano-roll covers a specific pitch range. This is useful for normalizing piano-rolls to a consistent pitch set.

    from miditoolkit.pianoroll import utils
    
    # cropping pitch via slicing
    pianoroll_small = pianoroll[:, 24:108]
    
    # padding pitch to a specific range (min_pitch, max_pitch)
    pianoroll_re = utils.pitch_padding(pianoroll_small, pitch_range=(24, 108))
  6. Convert MIDI notes to a piano-roll

    master

    To convert MIDI note data into a piano-roll representation, use miditoolkit.midi.parser.MidiFile to load the file and access the notes from an instrument, then pass those notes to miditoolkit.pianoroll.parser.notes2pianoroll. The resulting piano-roll is a 2D array where the axes represent time (ticks) and pitch.

    from miditoolkit.midi import parser as mid_parser
    from miditoolkit.pianoroll import parser as pr_parser
    
    # load midi file
    path_midi = 'path/to/your/file.midi'
    mido_obj = mid_parser.MidiFile(path_midi)
    notes = mido_obj.instruments[0].notes
    
    # convert notes to pianoroll
    pianoroll = pr_parser.notes2pianoroll(notes)
    
    # pianoroll shape is (ticks, pitch)
    print(pianoroll.shape)
  7. Resample piano-rolls and notes

    master

    Resampling allows you to change the temporal resolution of your data.

    1. Downsampling: When converting notes to a piano-roll, provide a resample_factor to miditoolkit.pianoroll.parser.notes2pianoroll. The factor is calculated as new_resolution / old_resolution.
    2. Upsampling: To convert a piano-roll back into MIDI notes with a different resolution, use miditoolkit.pianoroll.parser.pianoroll2notes and provide the inverse factor (1 / resample_factor).
    from miditoolkit.midi import parser as mid_parser
    from miditoolkit.pianoroll import parser as pr_parser
    
    # Setup resolution factors
    old_resol = mido_obj.ticks_per_beat
    new_resol = 24
    factor_down = new_resol / old_resol
    
    # Downsample: notes -> pianoroll
    pianoroll = pr_parser.notes2pianoroll(notes, resample_factor=factor_down)
    
    # Upsample: pianoroll -> notes
    notes_re = pr_parser.pianoroll2notes(pianoroll, resample_factor=1/factor_down)
  8. Parse a MIDI file using MidiFile

    master

    To parse an existing MIDI file, use miditoolkit.midi.parser.MidiFile. Once loaded, you can access various musical properties including ticks per beat, time signatures, key signatures, tempo changes, markers, and instrument tracks.

    Key attributes available on the MidiFile object:

    • ticks_per_beat: The resolution of the MIDI file.
    • time_signature_changes: A list of time signature change events.
    • key_signature_changes: A list of key signature change events.
    • tempo_changes: A list of tempo change events.
    • markers: A list of marker events.
    • instruments: A list of Instrument objects representing the tracks.
    from miditoolkit.midi import parser as mid_parser
    
    # load midi file
    path_midi = 'path/to/your/file.midi'
    mido_obj = mid_parser.MidiFile(path_midi)
    
    print('ticks per beat:', mido_obj.ticks_per_beat)
    print('time signatures:', mido_obj.time_signature_changes)
    print('tempo changes:', mido_obj.tempo_changes)
    print('markers:', mido_obj.markers)
    print('instruments:', mido_obj.instruments)
  9. Convert MIDI ticks to seconds

    master

    To convert a MIDI tick position to a time in seconds, use the get_tick_to_time_mapping() method on the MidiFile object. This returns a mapping (dictionary or similar) that allows you to look up the time in seconds for any given tick.

    Example workflow:

    1. Access a note from an instrument.
    2. Get the start tick of that note.
    3. Retrieve the mapping from the MidiFile object.
    4. Use the tick as a key in the mapping to get the seconds.
    # convert to seconds
    # Assuming mido_obj is a loaded MidiFile
    note = mido_obj.instruments[0].notes[20]
    mapping = mido_obj.get_tick_to_time_mapping()
    tick = note.start
    sec = mapping[tick]
    print(f'{tick} tick at {sec} seconds')
  10. Crop a MIDI segment and export

    master

    You can export a specific segment of a MIDI file by using the dump method with the segment parameter. The segment parameter expects a tuple of (start_tick, end_tick).

    To calculate ticks based on musical bars, multiply the ticks_per_beat by the number of beats per bar and the bar index.

    # load midi file
    path_midi = 'testcases/2.midi'
    mido_obj = mid_parser.MidiFile(path_midi)
    beat_resol = mido_obj.ticks_per_beat
    
    # define interval: from 2nd to 8th bar (assuming 4 beats per bar)
    st = beat_resol * 4 * 2
    ed = beat_resol * 4 * 8
    
    # export the segment
    mido_obj.dump('seg.midi', segment=(st, ed))