Quick Start
Installation
Pre-Built Version
pip install symusic
We have pre-built wheels for Linux
, macOS
, and Windows
, with support for both X86
and ARM
architectures (Windows ARM64 is not supported yet).
The wheels are built with 3.8
, 3.9
, 3.10
, 3.11
and 3.12
versions of CPython
, and 3.9
version of PyPy
.
Note that there are some memory leaks in CPython 3.12
, and they would be reported by the nanobind
framework when you import symusic
in python 3.12.
I hope that the CPython
team would fix them soon.
From Source
For building from source, you could directly install from the source distribution on PyPI
:
pip install --no-binary symusic symusic
Or you could clone the repository and install it:
git clone --recursive git@github.com:Yikai-Liao/symusic.git
pip install ./symusic
Note that symusic
is built with C++20
, and it does need some modern features, so we need to check the compatibility of the compiler.
- gcc:
>=11
- clang: :
>=15
- msvc: Not tested, but it's recommended to use
Visual Studio 2022
On Linux, if you don't have the sudo
permission, you could install a gcc
compiler from conda
:
conda install conda-forge::gcc conda-forge::gxx
And then set the CC
and CXX
environment variables to the path of the gcc
and g++
:
CC=/path_to_conda_root/envs/env_name/bin/gcc CXX=/path_to_conda_root/envs/env_name/bin/g++ pip install --no-binary symusic symusic
Load
If you pass a file path (str or pathlib.Path) to the Score
constructor,
it would be dynamically dispatched to the from_file
method. symusic
will try to infer the file format from the file extension,
or you can specify the format by passing the fmt
argument.
from symusic import Score
Score("xxx.mid")
Score("xxx.txt", fmt="abc")
Score.from_file("xxx.mid")
Score.from_file("xxx.txt", fmt="abc")
You could also pass a ttype
argument to the Score
constructor to specify the type of time unit.
There are three types of time unit: tick
, quarter
and second
, while the default is tick
.
A member function to
is also provided for converting the time unit of the score.
tick
(int32) is just the raw time unit in the MIDI file.quarter
(float32) is a normalized time unit (directly normalized fromtick
usingticks_per_quarter
), where the length of a quarter note is 1.second
(float32) means the time unit is in seconds, the same as whatpretty_midi
does.
from symusic import Score, TimeUnit
# you could specify the time unit when loading a score
Score("xxx.mid", ttype=TimeUnit.quarter)
Score("xxx.txt", fmt="abc", ttype='second')
# or convert the time unit after loading
# an additional argument `min_dur` is optional, which is the minimum duration of a note
Score("xxx.mid", "quarter", ).to("tick", min_dur=1)
Score("xxx.txt", fmt="abc").to(TimeUnit.second)
Dump
Currently, we got two dumpers, dump_midi
and dump_abc
, which dump the score to a MIDI file and an ABC file respectively.
Before actually dumping the score, it would be automatically converted to the time unit tick
, which is the raw time unit in the MIDI file.
So if you want to specify a minimum duration of a note, you need to call the to
method manually before dumping.
from symusic import Score
s = Score("xxx.mid")
s.dump_midi("out.mid")
s.dump_abc("out.abc")
Pickle & Multi Processing
All the objects in symusic
are pickleable (extremely fast because of the zpp_bits
backend we use),
so you can use multiprocessing
to accelerate the processing.
Note that because of zpp_bits
, pickle would be faster than loading from or dumping to a MIDI file.
from symusic import Score
import pickle
import multiprocessing as mp
s = Score("xxx.mid")
with open("out.pkl", "wb") as f:
pickle.dump(s, f)
with open("out.pkl", "rb") as f:
s = pickle.load(f)
with mp.Pool(4) as pool:
print(pool.map(lambda x: x.to("second"), [s, s, s, s]))
Accessing the Score
Basic Information
The data structure of Score
is quite simple, just following what miditoolkit
and pretty_midi
do.
from symusic import Score
s = Score("xxx.mid")
# ttype is a property of all the objects in symusic related to time
s.ttype # the type of time unit, 'tick', 'quarter' or 'second'
s.ticks_per_quarter # or s.tpq
# Global Events
s.tempos # list of tempo changes
s.time_signatures # list of time signature changes
s.key_signatures # list of key signature changes
s.lyrics # list of lyrics
s.markers # list of markers
s.start() # return the start time of the score
s.end() # return the end time of the score
s.empty() # return True if the score is empty
s.note_num() # return the number of notes in the score
# Tracks
s.tracks # list of tracks
t = s.tracks[0]
t.ttype # read-only property, the same as s.ttype
t.name # track name
t.program # program number
t.is_drum # whether it's a drum track
t.notes # list of notes
t.controls # list of control changes
t.pitch_bends # list of pitch bends
t.pedals # list of pedals
t.start() # return the start time of the track
t.end() # return the end time of the track
t.empty() # return True if the track is empty
t.note_num() # return the number of notes in the track
# Note, the most important event
n = t.notes[0]
n.ttype # read-only property, the same as s.ttype
n.time # the NoteOn time or n.start
n.start # the same as n.time
n.duration # the duration of the note
n.end # a read-only property, n.time + n.duration
n.end_time() # a method that returns n.end
n.pitch # the pitch of the note
n.velocity # the velocity of the note
n.empty() # duration <= 0 or velocity <= 0
The container of events in symusic
like the NoteList is a binding of std::vector
in c++, with a similar interface to list in python.
We also add some methods to the container, like start
, end
, empty
:
from symusic import Score
notes = Score("xxx.mid").tracks[0].notes
notes.empty() # return True if the note list is empty
notes.start() # return the start time of the note list
notes.end() # return the end time of the note list
notes1 = notes.copy() # copy the note list
# the default key is (time, duration, pitch) for notes, (time, duration) for pedals
# for other events, the default key is "time"
notes2 = notes.sort(reversed=False, inplace=False) # sort the notes
notes3 = notes.sort(lambda x: x.velocity) # you could also customize the key, but it is slower
notes4 = notes.filter(lambda x: x.pitch > 60, inplace=False) # filter the notes
# adjust the time of the notes, the first list is the old time, the second list is the new time
# the time unit of the two lists should be the same as the time unit of the note list
# the semantic of adjust_time is the same as the method in pretty_midi
notes5 = notes.adjust_time([0, 10, notes.end()], [0, 20, notes.end() / 2])
Struct of Array (SOA)
symusic
has a great numpy support. You could easily convert "a list of objects" in symusic
to "a dict of numpy array`.
from symusic import Score
import numpy as np
from typing import Dict
s = Score("xxx.mid")
# get the numpy arrays
tempos: Dict[str, np.ndarray] = s.tempos.numpy()
notes: Dict[str, np.ndarray] = s.tracks[0].notes.numpy()
# access the array in dict
mspq = tempos["mspq"] # a 1D numpy array, dtype is int32. mspq for microsecond per quarter note
start = notes["time"] # a 1D numpy array, the dtype is determined by the time unit(ttype) of the score
# you could also convert the numpy arrays back to the list of objects
from symusic import Note
note_list1 = Note.from_numpy(**notes, ttype=s.ttype)
note_list2 = Note.from_numpy(
time=notes["time"], duration=notes["duration"], pitch=notes["pitch"], velocity=notes["velocity"], ttype=s.ttype
)
# The note_list you get here is the same as s.tracks[0].notes
Piano Roll
symusic
also supports a conversion to the piano roll, which is a 3D numpy array.
But convert the piano roll back to the score is not supported yet since the complexity of the piano roll.
from symusic import Score
s = Score("xxx.mid")
# You'd better resample the score before converting it to the piano roll to reduce the size of it
# tpq=6 means that the minimum time unit is 1/6 quarter note or 1/24 note
# min_dur=1 means that the duration of a note is at least 1 time unit
s_quantized = s.resample(tpq=6, min_dur=1)
# 4D np array, [modes, tracks, pitch, time]
s_pianoroll = s_quantized.pianoroll(
# the modes of the piano roll, which determines the "modes dim"
# only the following modes are supported: "onset", "frame", "offset"
# you could determine the order by yourself
modes=["onset", "frame", "offset"], # List[str]
pitch_range=[0, 128], # the pitch range (half-open interval) of the piano roll, [0, 128) by default
encode_velocity=True # make the pianoroll binary or filled with velocity
)
# 3D np array, [modes, pitch, time]
t_pianoroll = s_quantized.tracks[0].pianoroll(
modes=["onset", "frame", "offset"], pitch_range=[0, 128], encode_velocity=True
)
You could also visualize the piano roll using matplotlib
:
from symusic import Score
from matplotlib import pyplot as plt
s = Score("xxx.mid").resample(tpq=6, min_dur=1)
track = s.tracks[0]
pianoroll = track.pianoroll(modes=["onset", "frame"], pitch_range=[0, 128], encode_velocity=False)
# this will show the onset and frame of the piano roll in one figure
plt.imshow(pianoroll[0] + pianoroll[1], aspect="auto")
plt.show()
Synthesis
From version 0.4.0, symusic
supports a simple synthesis of the score using SoundFont(2/3).
It's our own implementation, called prestosynth
, and you could find the repository here.
from symusic import Score, Synthesizer, BuiltInSF3 ,dump_wav
s = Score("xxx.mid")
# You could choose a builtin soundfont
# And the following one is the default soundfont if you don't specify it when creating a synthesizer
sf_path = BuiltInSF3.MuseScoreGeneral().path(download=True)
# sf3 and sf2 are both supported
sf_path = "path/to/your/soundfont.sf3"
synth = Synthesizer(
sf_path = sf_path, # the path to the soundfont
sample_rate = 44100, # the sample rate of the output wave, 44100 is the default value
)
# audio is a 2D numpy array of float32, [channels, time]
audio = synth.render(s, stereo=True) # stereo is True by default, which means you will get a stereo wave
# you could also dump the wave to a file
# use_int16 is True by default, which means the output wave is int16, otherwise float32
dump_wav("out.wav", audio, sample_rate=44100, use_int16=True)