MIDI File Operations Tutorial

This tutorial covers the basics of loading, inspecting, modifying, and saving MIDI files using Symusic.

Loading MIDI Files

The primary way to load a MIDI file is using the Score constructor or the Score.from_file class method.

from symusic import Score, TimeUnit

# Load a MIDI file (defaults to 'tick' time unit)
score_tick = Score("path/to/your/midi_file.mid")

# Check the time unit and ticks per quarter note
print(f"Time unit: {score_tick.ttype}")
print(f"Ticks per quarter: {score_tick.ticks_per_quarter}")

# Load specifying a different time unit
score_quarter = Score("path/to/your/midi_file.mid", ttype="quarter")
print(f"Time unit: {score_quarter.ttype}")

score_second = Score.from_file("path/to/your/midi_file.mid", ttype=TimeUnit.second)
print(f"Time unit: {score_second.ttype}")

Inspecting Score Contents

Once loaded, you can explore the Score object's contents.

# Basic information
print(f"Number of tracks: {score_tick.track_num()}")
print(f"Total notes: {score_tick.note_num()}")
print(f"Score duration (ticks): {score_tick.end() - score_tick.start()}")

# Access tracks
print("\nTracks:")
for i, track in enumerate(score_tick.tracks):
    print(f"  Track {i}: Name='{track.name}', Program={track.program}, IsDrum={track.is_drum}, Notes={track.note_num()}")

# Access global events (e.g., tempos)
print("\nTempo Changes:")
for tempo in score_tick.tempos:
    print(f"  Time={tempo.time}, QPM={tempo.qpm}")

# Access notes in the first track
first_track = score_tick.tracks[0]
print("\nFirst 5 notes of Track 0:")
for note in first_track.notes[:5]:
    print(f"  Time={note.time}, Dur={note.duration}, Pitch={note.pitch}, Vel={note.velocity}")

Modifying MIDI Data

Symusic provides methods for various modifications. Most methods have an inplace option.

Transposition

# Transpose the entire score up by 2 semitones (creates a new Score)
transposed_score = score_tick.shift_pitch(2)

# Transpose only the first track down by 1 semitone (inplace)
score_tick.tracks[0].shift_pitch_inplace(-1)

Time Adjustment

# Shift all events later by 960 ticks (inplace)
score_tick.shift_time_inplace(960)

# Clip the score to keep only events between time 1920 and 3840
clipped_score = score_tick.clip(1920, 3840)

Changing Tempo

from symusic import Tempo

# Change the tempo at the beginning
if score_tick.tempos:
    score_tick.tempos[0].qpm = 100.0 # Set first tempo to 100 BPM
else:
    # Add a tempo event if none exist
    score_tick.tempos.append(Tempo(time=0, qpm=100.0))

# Add a gradual tempo change (ritardando)
# Convert to seconds for easier tempo manipulation if needed
# score_second = score_tick.to("second")
# ... manipulate tempo in seconds ...
# score_tick = score_second.to("tick")

Removing Tracks

# Remove the last track (inplace)
if score_tick.tracks:
    score_tick.tracks.pop(-1)

# Remove drum tracks (creates a new list)
non_drum_tracks = [track for track in score_tick.tracks if not track.is_drum]
score_tick.tracks = non_drum_tracks # Replace the track list

Saving MIDI Files

Save the modified (or original) Score object back to a MIDI file.

# Save the score to a new MIDI file
# Note: Saving always uses the 'tick' representation internally.
# If the score is not in 'tick', it will be converted automatically.
transposed_score.dump_midi("transposed_output.mid")

# Save the clipped score
clipped_score.dump_midi("clipped_output.mid")

Symusic automatically handles the conversion to the tick-based format required for MIDI files during the dump_midi process.

Batch Processing MIDI Files

Symusic's speed makes it ideal for processing large collections of MIDI files, often combined with Python's multiprocessing.

import multiprocessing as mp
from pathlib import Path
from symusic import Score

def process_midi(midi_path: Path, output_dir: Path):
    try:
        score = Score(midi_path)
        # Example: Keep only piano tracks (program 0)
        piano_tracks = [t for t in score.tracks if t.program == 0 and not t.is_drum]
        if not piano_tracks:
            return # Skip if no piano tracks

        score.tracks = piano_tracks
        score.dump_midi(output_dir / midi_path.name)
        print(f"Processed: {midi_path.name}")
    except Exception as e:
        print(f"Error processing {midi_path.name}: {e}")

if __name__ == "__main__":
    input_directory = Path("path/to/midi/collection")
    output_directory = Path("path/to/output/dir")
    output_directory.mkdir(exist_ok=True)

    midi_files = list(input_directory.glob("*.mid"))

    # Create tuples of arguments for starmap
    args = [(mf, output_directory) for mf in midi_files]

    # Use multiprocessing pool
    with mp.Pool(processes=mp.cpu_count()) as pool:
        pool.starmap(process_midi, args)

    print("Batch processing complete.")

This tutorial provides a starting point for working with MIDI files in Symusic. Explore the Score and Track API documentation for more advanced methods and options.