Quickstart

This guide walks through the most common Python workflow: install Symusic, load a MIDI file, inspect and transform notes, and export the results. Every example runs on top of the same C++20 core that powers the library in production, so the snippets scale from a notebook to a training pipeline.

Install Symusic

Pre-built wheels

pip install symusic

We publish CPython wheels for Python 3.9 through 3.14 across Linux, macOS, and Windows, including Windows ARM64. PyPy wheels are currently published for pp311 on manylinux_x86_64, macosx_x86_64, and macosx_arm64.

CPython 3.12 can still emit harmless nanobind leak warnings. If they become distracting during local work, use 3.11 until the upstream warning noise is reduced.

Build from source

pip install --no-binary symusic symusic  # from PyPI source distribution

or clone the repository if you need the latest commit:

git clone --recursive https://github.com/Yikai-Liao/symusic
cd symusic
pip install .

Symusic targets C++20. Minimum compiler versions: GCC 11, Clang 15, MSVC 2022. On Linux machines without sudo you can install the toolchain via conda and wire it in with CC=... CXX=....

Load a score

from symusic import Score

score = Score("example.mid")         # format auto-detected
score_abc = Score("example.abc", fmt="abc")

Passing a string or Path dispatches to Score.from_file. To work in a specific time unit, use the ttype argument or the convenience constructor names:

from symusic import Score, TimeUnit

score_tick = Score("example.mid", ttype="tick")
score_quarter = Score("example.mid", ttype=TimeUnit.quarter)
score_second = score_tick.to("second")          # convert after loading
  • tick retains the raw MIDI ticks (int32).

  • quarter normalizes time so that a quarter note equals 1.0.

  • second evaluates the tempo map and produces wall-clock seconds.

The Score.to(new_unit, min_dur=None) method converts the entire hierarchy—tracks, notes, pedals, etc.—to a different unit. Use min_dur when converting to ticks to avoid rounding durations to zero.

Beat and downbeat helpers

from symusic import Score

score = Score("example.mid").to("quarter")
beats = score.get_beats()
downbeats = score.get_downbeats()

get_beats() returns the musical beat grid implied by the tempo and time-signature maps. get_downbeats() keeps only bar starts. Both helpers return NumPy arrays in the score’s current time unit, so they work naturally in tick, quarter, or second workflows.

Save back to disk

score = Score("example.mid")
score.shift_pitch_inplace(1)
score.dump_midi("example_shifted.mid")
score.dump_abc("example.txt")

dump_midi internally works in ticks, so a score in quarters/seconds will be converted before writing. For ABC export, set fmt="abc" when loading so the parser sees the correct metadata.

Pickling and multiprocessing

Every container in Symusic is pickleable. We serialize through zpp_bits, which makes pickling far faster than writing out intermediate MIDI files.

import multiprocessing as mp
import pickle
from symusic import Score

score = Score("example.mid")
with open("score.pkl", "wb") as fh:
    pickle.dump(score, fh)

with mp.Pool(4) as pool:
    doubled = pool.map(lambda s: s.shift_pitch(12), [score] * 4)

Inspect the score hierarchy

from symusic import Score

score = Score("example.mid")
print(score.ticks_per_quarter)
print(len(score.tracks))
first_track = score.tracks[0]
print(first_track.name, first_track.program, "drum" if first_track.is_drum else "melodic")
print(len(first_track.notes), len(first_track.controls))

Score owns lists of global events (tempos, time_signatures, key_signatures, markers) as well as a list of Track objects. Each Track contains lists (NoteList, ControlChangeList, PitchBendList, PedalList, TextMetaList) that expose list-like APIs plus helpers such as .start(), .end(), .empty(), .copy(), .sort(), and .filter().

Structured NumPy (SoA) exports

Event lists can produce a struct of arrays view that plays nicely with NumPy or PyTorch:

from typing import Dict
import numpy as np
from symusic import Score, Note

score = Score("example.mid")
notes: Dict[str, np.ndarray] = score.tracks[0].notes.numpy()
print(notes.keys())  # dict of time, duration, pitch, velocity arrays

reconstructed = Note.from_numpy(**notes, ttype=score.ttype)
assert reconstructed == score.tracks[0].notes

This pattern avoids Python loops when batching features for machine-learning pipelines.

Piano roll conversion

from symusic import Score

score = Score("example.mid").resample(tpq=6, min_dur=1)
roll = score.pianoroll(modes=["onset", "frame"], pitch_range=[0, 128], encode_velocity=True)
track_roll = score.tracks[0].pianoroll(modes=["onset", "frame", "offset"], pitch_range=[24, 108])
  • Use Score.resample(tpq, min_dur) before converting to limit the time axis.

  • modes chooses which binary/velocity channels to emit.

  • pitch_range controls the pitch axis [low, high).

Visualization example:

from matplotlib import pyplot as plt
plt.imshow(track_roll[0] + track_roll[1], aspect="auto")
plt.show()

Synthesis with SoundFonts

from symusic import Score, Synthesizer, BuiltInSF3, dump_wav

score = Score("example.mid")
sf_path = BuiltInSF3.MuseScoreGeneral().path(download=True)
synth = Synthesizer(sf_path=sf_path, sample_rate=44100, quality=0)
wave = synth.render(score, stereo=True)  # returns np.ndarray[channel, samples]
dump_wav("example.wav", wave, sample_rate=44100, use_int16=True)

Symusic ships a lightweight SoundFont manager (BuiltInSF2/SF3) that can download curated fonts on demand. Rendering internally converts the score to seconds, maps tracks to instruments via program and is_drum, and streams the samples through Prestosynth. Use the returned NumPy array directly in post-processing pipelines or dump a WAV with dump_wav.