#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This module contains music related utilities
"""
from __future__ import annotations
import copy
from collections import defaultdict
import re
import warnings
import numpy as np
from scipy.interpolate import interp1d
from scipy.sparse import csc_matrix
from typing import Union, Callable, Optional, TYPE_CHECKING
from partitura.utils.generic import find_nearest, search, iter_current_next
import partitura
from tempfile import TemporaryDirectory
import os
try:
import miditok
from miditok.midi_tokenizer import MIDITokenizer
import miditoolkit
except ImportError:
miditok = None
miditoolkit = None
class MIDITokenizer(object):
pass
from partitura.utils.misc import deprecated_alias
if TYPE_CHECKING:
# Import typing info for typing annotations.
# For this to work we need to import annotations from __future__
# Solution from
# https://medium.com/quick-code/python-type-hinting-eliminating-importerror-due-to-circular-imports-265dfb0580f8
from partitura.score import ScoreLike, Interval
from partitura.performance import PerformanceLike, Performance, PerformedPart
MIDI_BASE_CLASS = {"c": 0, "d": 2, "e": 4, "f": 5, "g": 7, "a": 9, "b": 11}
# _MORPHETIC_BASE_CLASS = {'c': 0, 'd': 1, 'e': 2, 'f': 3, 'g': 4, 'a': 5, 'b': 6}
# _MORPHETIC_OCTAVE = {0: 32, 1: 39, 2: 46, 3: 53, 4: 60, 5: 67, 6: 74, 7: 81, 8: 89}
ALTER_SIGNS = {None: "", 0: "", 1: "#", 2: "x", -1: "b", -2: "bb"}
DUMMY_PS_BASE_CLASS = {
0: ("c", 0),
1: ("c", 1),
2: ("d", 0),
3: ("d", 1),
4: ("e", 0),
5: ("f", 0),
6: ("f", 1),
7: ("g", 0),
8: ("g", 1),
9: ("a", 0),
10: ("a", 1),
11: ("b", 0),
}
MEI_DURS_TO_SYMBOLIC = {
"long": "long",
"0": "breve",
"breve": "breve",
"1": "whole",
"2": "half",
"4": "quarter",
"8": "eighth",
"16": "16th",
"32": "32nd",
"64": "64th",
"128": "128th",
"256": "256th",
}
SYMBOLIC_TO_INT_DURS = {
"long": 0.25,
"breve": 0.5,
"whole": 1,
"half": 2,
"quarter": 4,
"eighth": 8,
"16th": 16,
"32nd": 32,
"64th": 64,
"128th": 128,
"256th": 256,
}
LABEL_DURS = {
"long": 16,
"breve": 8,
"whole": 4,
"half": 2,
"h": 2,
"quarter": 1,
"q": 1,
"eighth": 1 / 2,
"e": 1 / 2,
"16th": 1 / 4,
"32nd": 1 / 8.0,
"64th": 1 / 16,
"128th": 1 / 32,
"256th": 1 / 64,
}
DOT_MULTIPLIERS = (1, 1 + 1 / 2, 1 + 3 / 4, 1 + 7 / 8)
# DURS and SYM_DURS encode the same information as _LABEL_DURS and
# _DOT_MULTIPLIERS, but they allow for faster estimation of symbolic duration
# (estimate_symbolic duration). At some point we will probably do away with
# _LABEL_DURS and _DOT_MULTIPLIERS.
DURS = np.array(
[
1.5625000e-02,
2.3437500e-02,
2.7343750e-02,
2.9296875e-02,
3.1250000e-02,
4.6875000e-02,
5.4687500e-02,
5.8593750e-02,
6.2500000e-02,
9.3750000e-02,
1.0937500e-01,
1.1718750e-01,
1.2500000e-01,
1.8750000e-01,
2.1875000e-01,
2.3437500e-01,
2.5000000e-01,
3.7500000e-01,
4.3750000e-01,
4.6875000e-01,
5.0000000e-01,
5.0000000e-01,
7.5000000e-01,
7.5000000e-01,
8.7500000e-01,
8.7500000e-01,
9.3750000e-01,
9.3750000e-01,
1.0000000e00,
1.0000000e00,
1.5000000e00,
1.5000000e00,
1.7500000e00,
1.7500000e00,
1.8750000e00,
1.8750000e00,
2.0000000e00,
2.0000000e00,
3.0000000e00,
3.0000000e00,
3.5000000e00,
3.5000000e00,
3.7500000e00,
3.7500000e00,
4.0000000e00,
6.0000000e00,
7.0000000e00,
7.5000000e00,
8.0000000e00,
1.2000000e01,
1.4000000e01,
1.5000000e01,
1.6000000e01,
2.4000000e01,
2.8000000e01,
3.0000000e01,
]
)
SYM_DURS = [
{"type": "256th", "dots": 0},
{"type": "256th", "dots": 1},
{"type": "256th", "dots": 2},
{"type": "256th", "dots": 3},
{"type": "128th", "dots": 0},
{"type": "128th", "dots": 1},
{"type": "128th", "dots": 2},
{"type": "128th", "dots": 3},
{"type": "64th", "dots": 0},
{"type": "64th", "dots": 1},
{"type": "64th", "dots": 2},
{"type": "64th", "dots": 3},
{"type": "32nd", "dots": 0},
{"type": "32nd", "dots": 1},
{"type": "32nd", "dots": 2},
{"type": "32nd", "dots": 3},
{"type": "16th", "dots": 0},
{"type": "16th", "dots": 1},
{"type": "16th", "dots": 2},
{"type": "16th", "dots": 3},
{"type": "eighth", "dots": 0},
{"type": "e", "dots": 0},
{"type": "eighth", "dots": 1},
{"type": "e", "dots": 1},
{"type": "eighth", "dots": 2},
{"type": "e", "dots": 2},
{"type": "eighth", "dots": 3},
{"type": "e", "dots": 3},
{"type": "quarter", "dots": 0},
{"type": "q", "dots": 0},
{"type": "quarter", "dots": 1},
{"type": "q", "dots": 1},
{"type": "quarter", "dots": 2},
{"type": "q", "dots": 2},
{"type": "quarter", "dots": 3},
{"type": "q", "dots": 3},
{"type": "half", "dots": 0},
{"type": "h", "dots": 0},
{"type": "half", "dots": 1},
{"type": "h", "dots": 1},
{"type": "half", "dots": 2},
{"type": "h", "dots": 2},
{"type": "half", "dots": 3},
{"type": "h", "dots": 3},
{"type": "whole", "dots": 0},
{"type": "whole", "dots": 1},
{"type": "whole", "dots": 2},
{"type": "whole", "dots": 3},
{"type": "breve", "dots": 0},
{"type": "breve", "dots": 1},
{"type": "breve", "dots": 2},
{"type": "breve", "dots": 3},
{"type": "long", "dots": 0},
{"type": "long", "dots": 1},
{"type": "long", "dots": 2},
{"type": "long", "dots": 3},
]
MAJOR_KEYS = [
"Cb",
"Gb",
"Db",
"Ab",
"Eb",
"Bb",
"F",
"C",
"G",
"D",
"A",
"E",
"B",
"F#",
"C#",
]
MINOR_KEYS = [
"Ab",
"Eb",
"Bb",
"F",
"C",
"G",
"D",
"A",
"E",
"B",
"F#",
"C#",
"G#",
"D#",
"A#",
]
TIME_UNITS = ["beat", "quarter", "sec", "div"]
NOTE_NAME_PATT = re.compile(r"([A-G]{1})([xb\#]*)(\d+)")
INTERVALCLASSES = [
f"{specific}{generic}"
for generic in [2, 3, 6, 7]
for specific in ["dd", "d", "m", "M", "A", "AA"]
] + [
f"{specific}{generic}"
for generic in [1, 4, 5]
for specific in ["dd", "d", "P", "A", "AA"]
]
INTERVAL_TO_SEMITONES = dict(
zip(
INTERVALCLASSES,
[
generic + specific
for generic in [1, 3, 8, 10]
for specific in [-2, -1, 0, 1, 2, 3]
]
+ [
generic + specific
for generic in [0, 5, 7]
for specific in [-2, -1, 0, 1, 2]
],
)
)
STEPS = {
"C": 0,
"D": 1,
"E": 2,
"F": 3,
"G": 4,
"A": 5,
"B": 6,
0: "C",
1: "D",
2: "E",
3: "F",
4: "G",
5: "A",
6: "B",
}
MUSICAL_BEATS = {6: 2, 9: 3, 12: 4}
# Standard tuning frequency of A4 in Hz
A4 = 440.0
[docs]def ensure_notearray(notearray_or_part, *args, **kwargs):
"""
Ensures to get a structured note array from the input.
Parameters
----------
notearray_or_part : structured ndarray, `Score`, `Part`, `PerformedPart`
Input score information
kwargs : dict
Additional arguments to be passed to `partitura.utils.note_array_from_part()`.
Returns
-------
structured ndarray
Structured array containing score information.
"""
from partitura.score import Part, PartGroup, Score
from partitura.performance import PerformedPart, Performance
if isinstance(notearray_or_part, np.ndarray):
if notearray_or_part.dtype.fields is not None:
return notearray_or_part
else:
raise ValueError("Input array is not a structured array!")
elif isinstance(notearray_or_part, Part):
return note_array_from_part(notearray_or_part, *args, **kwargs)
elif isinstance(notearray_or_part, PartGroup):
return note_array_from_part_list(notearray_or_part.children, *args, **kwargs)
elif isinstance(notearray_or_part, Score):
return note_array_from_part_list(notearray_or_part.parts, *args, **kwargs)
elif isinstance(notearray_or_part, (PerformedPart, Performance)):
return notearray_or_part.note_array(*args, **kwargs)
elif isinstance(notearray_or_part, Score):
return notearray_or_part.note_array(*args, **kwargs)
elif isinstance(notearray_or_part, list):
if all([isinstance(part, Part) for part in notearray_or_part]):
return note_array_from_part_list(notearray_or_part, *args, **kwargs)
else:
raise ValueError(
"`notearray_or_part` should be a list of "
"`Part` objects, but was given "
"[{0}]".format(",".join(str(type(p)) for p in notearray_or_part))
)
else:
raise ValueError(
"`notearray_or_part` should be a structured "
"numpy array, a `Part`, `PartGroup`, a "
"`PerformedPart`, or a list but "
"is {0}".format(type(notearray_or_part))
)
[docs]def ensure_rest_array(restarray_or_part, *args, **kwargs):
"""
Ensures to get a structured note array from the input.
Parameters
----------
restarray_or_part : structured ndarray, `Part` or `PerformedPart`
Input score information
Returns
-------
structured ndarray
Structured array containing score information.
"""
from partitura.score import Part, PartGroup
if isinstance(restarray_or_part, np.ndarray):
if restarray_or_part.dtype.fields is not None:
return restarray_or_part
else:
raise ValueError("Input array is not a structured array!")
elif isinstance(restarray_or_part, Part):
return rest_array_from_part(restarray_or_part, *args, **kwargs)
elif isinstance(restarray_or_part, PartGroup):
return rest_array_from_part_list(restarray_or_part.children, *args, **kwargs)
elif isinstance(restarray_or_part, list):
if all([isinstance(part, Part) for part in restarray_or_part]):
return rest_array_from_part_list(restarray_or_part, *args, **kwargs)
else:
raise ValueError(
"`restarray_or_part` should be a list of "
"`Part` objects, but was given "
"[{0}]".format(",".join(str(type(p)) for p in restarray_or_part))
)
else:
raise ValueError(
"`restarray_or_part` should be a structured "
"numpy array, a `Part`, `PartGroup`, or a list but "
"is {0}".format(type(restarray_or_part))
)
def transpose_step(step, interval, direction):
"""
Transpose a note by a given interval.
Parameters
----------
step
inverval
"""
op = lambda x, y: abs(x + y) % 7 if direction == "up" else abs(x - y) % 7
if interval == "P1":
pass
else:
step = STEPS[op(STEPS[step.capitalize()], interval - 1)]
return step
def _transpose_note(note, interval):
"""
Transpose a note by a given interval.
Parameters
----------
note
inverval
"""
if interval.quality + str(interval.number) == "P1":
pass
else:
# TODO work for arbitrary octave.
prev_step = note.step.capitalize()
note.step = transpose_step(prev_step, interval.number, interval.direction)
if STEPS[note.step] - STEPS[prev_step] < 0 and interval.direction == "up":
note.octave += 1
elif STEPS[note.step] - STEPS[prev_step] > 0 and interval.direction == "down":
note.octave -= 1
else:
note.octave = note.octave
prev_alter = note.alter if note.alter is not None else 0
prev_pc = MIDI_BASE_CLASS[prev_step.lower()] + prev_alter
tmp_pc = MIDI_BASE_CLASS[note.step.lower()]
if interval.direction == "up":
diff_sm = tmp_pc - prev_pc if tmp_pc >= prev_pc else tmp_pc + 12 - prev_pc
else:
diff_sm = prev_pc - tmp_pc if prev_pc >= tmp_pc else prev_pc + 12 - tmp_pc
note.alter = (
INTERVAL_TO_SEMITONES[interval.quality + str(interval.number)] - diff_sm
)
def transpose(score: ScoreLike, interval: Interval) -> ScoreLike:
"""
Transpose a score by a given interval.
Parameters
----------
score : ScoreLike
Score to be transposed.
interval : int
Interval to transpose by.
Returns
-------
Score
Transposed score.
"""
import partitura.score as s
import sys
# Copy needs to be deep, otherwise the recursion limit will be exceeded
old_recursion_depth = sys.getrecursionlimit()
sys.setrecursionlimit(10000)
# Deep copy of score
new_score = copy.deepcopy(score)
# Reset recursion limit to previous value to avoid side effects
sys.setrecursionlimit(old_recursion_depth)
if isinstance(score, s.Score):
for part in new_score.parts:
transpose(part, interval)
elif isinstance(score, s.Part):
for note in score.notes_tied:
_transpose_note(note, interval)
return new_score
def get_time_units_from_note_array(note_array):
fields = set(note_array.dtype.fields)
if fields is None:
raise ValueError("`note_array` must be a structured numpy array")
score_units = set(("onset_beat", "onset_quarter", "onset_div"))
performance_units = set(("onset_sec", "onset_tick"))
if len(score_units.intersection(fields)) > 0:
if "onset_beat" in fields:
return ("onset_beat", "duration_beat")
elif "onset_quarter" in fields:
return ("onset_quarter", "duration_quarter")
elif "onset_div" in fields:
return ("onset_div", "duration_div")
elif len(performance_units.intersection(fields)) > 0:
if "onset_sec" in fields:
return ("onset_sec", "duration_sec")
elif "onset_tick" in fields:
return ("onset_tick", "duration_tick")
else:
raise ValueError("Input array does not contain the expected " "time-units")
def pitch_spelling_to_midi_pitch(step, alter, octave):
midi_pitch = (octave + 1) * 12 + MIDI_BASE_CLASS[step.lower()] + (alter or 0)
return midi_pitch
def midi_pitch_to_pitch_spelling(midi_pitch):
octave = midi_pitch // 12 - 1
step, alter = DUMMY_PS_BASE_CLASS[np.mod(midi_pitch, 12)]
return ensure_pitch_spelling_format(step, alter, octave)
def note_name_to_pitch_spelling(note_name):
note_info = NOTE_NAME_PATT.search(note_name)
if note_info is None:
raise ValueError(
"Invalid note name. "
"The note name must be "
"'<pitch class>(alteration)<octave>', "
f"but was given {note_name}."
)
step, alter, octave = note_info.groups()
step, alter, octave = ensure_pitch_spelling_format(
step=step, alter=alter if alter != "" else "n", octave=int(octave)
)
return step, alter, octave
def note_name_to_midi_pitch(note_name):
step, alter, octave = note_name_to_pitch_spelling(note_name)
return pitch_spelling_to_midi_pitch(step, alter, octave)
def pitch_spelling_to_note_name(step, alter, octave):
f_alter = ""
if alter > 0:
if alter == 2:
f_alter = "x"
else:
f_alter = alter * "#"
elif alter < 0:
f_alter = abs(alter) * "b"
note_name = f"{step.upper()}{f_alter}{octave}"
return note_name
def midi_pitch_to_frequency(
midi_pitch: Union[int, float, np.ndarray], a4: Union[int, float] = A4
) -> Union[float, np.ndarray]:
"""
Convert MIDI pitch to frequency in Hz. This method assumes equal temperament.
Parameters
----------
midi_pitch: int, float or ndarray
MIDI pitch of the note(s).
a4 : int or float (optional)
Frequency of A4 in Hz. By default is 440 Hz.
Returns
-------
freq : float or ndarray
Frequency of the note(s).
"""
freq = (a4 / 32) * (2 ** ((midi_pitch - 9) / 12))
return freq
def frequency_to_midi_pitch(
freq: Union[int, float, np.ndarray],
a4: Union[int, float] = A4,
) -> Union[int, np.ndarray]:
"""
Convert frequency to MIDI pitch. This method assumes equal temperament.
Parameters
----------
freq : float, int or np.ndarray
Frequency of the note(s) in Hz.
a4 : int or float (optional)
Frequency of A4 in Hz. By default is 440 Hz.
Returns
-------
midi_pitch : int or np.ndarray
MIDI pitch of the notes.
"""
midi_pitch = np.round(12 * np.log2(32 * freq / a4) + 9)
if isinstance(midi_pitch, (int, float)):
return int(midi_pitch)
elif isinstance(midi_pitch, np.ndarray):
return midi_pitch.astype(int)
@deprecated_alias(t="time_in_seconds")
def seconds_to_midi_ticks(
time_in_seconds: Union[int, float, np.ndarray],
mpq=500000,
ppq=480,
) -> Union[int, np.ndarray]:
"""
Convert time in seconds to MIDI ticks
Parameters
----------
time_in_seconds : int, float or np.ndarray
Time in seconds
mpq : int
Microseconds per quarter (default is 500000)
ppq : int
Pulses per quarter note (default is 480)
Returns
-------
midi_ticks : int or np.ndarray
MIDI ticks. If the input was a float or an integer, the output
will be an integer. If the output was a numpy array, the output
will be a numpy array with dtype int.
"""
midi_ticks = np.round(1e6 * ppq * time_in_seconds / mpq)
if isinstance(time_in_seconds, np.ndarray):
return midi_ticks.astype(np.int)
else:
return int(midi_ticks)
def midi_ticks_to_seconds(
midi_ticks: Union[int, float, np.ndarray],
mpq=500000,
ppq=480,
) -> Union[float, np.ndarray]:
"""
Convert MIDI ticks to time in seconds
Parameters
----------
midi_ticks: int, float or np.ndarray
Time in MIDI ticks
mpq : int
Microseconds per quarter (default is 500000)
ppq : int
Pulses per quarter note (default is 480)
Returns
-------
time_in_seconds : int or np.ndarray
Time in seconds. If the input was a float or an integer, the output
will be a float. If the output was a numpy array, the output
will be a numpy array with dtype float.
"""
time_in_seconds = (mpq * midi_ticks) / float(1e6 * ppq)
return time_in_seconds
SIGN_TO_ALTER = {
"n": 0,
"ns": 1,
"nf": -1,
"#": 1,
"s": 1,
"ss": 2,
"x": 2,
"##": 2,
"###": 3,
"b": -1,
"f": -1,
"bb": -2,
"ff": -2,
"bbb": -3,
"-": None,
}
def ensure_pitch_spelling_format(step, alter, octave):
if step.lower() not in MIDI_BASE_CLASS:
if step.lower() != "r":
raise ValueError("Invalid `step`")
if isinstance(alter, str):
try:
alter = SIGN_TO_ALTER[alter]
except KeyError:
raise ValueError(
'Invalid `alter`, must be ("n", "#", '
'"x", "b" or "bb"), but given {0}'.format(alter)
)
if not isinstance(alter, int):
try:
alter = int(alter)
except TypeError or ValueError:
if alter is not None:
raise ValueError("`alter` must be an integer or None")
if octave == "-":
# check octave for weird rests in Batik match files
octave = None
else:
if not isinstance(octave, int):
try:
octave = int(octave)
except TypeError or ValueError:
if octave is not None:
raise ValueError("`octave` must be an integer or None")
return step.upper(), alter, octave
[docs]def fifths_mode_to_key_name(fifths, mode=None):
"""Return the key signature name corresponding to a number of sharps
or flats and a mode. A negative value for `fifths` denotes the
number of flats (i.e. -3 means three flats), and a positive
number the number of sharps. The mode is specified as 'major'
or 'minor'. If `mode` is None, the key is assumed to be major.
Parameters
----------
fifths : int
Number of fifths
mode : {'major', 'minor', None, -1, 1}
Mode of the key signature
Returns
-------
str
The name of the key signature, e.g. 'Am'
Examples
--------
>>> fifths_mode_to_key_name(0, 'minor')
'Am'
>>> fifths_mode_to_key_name(0, 'major')
'C'
>>> fifths_mode_to_key_name(3, 'major')
'A'
>>> fifths_mode_to_key_name(-1, 1)
'F'
"""
global MAJOR_KEYS, MINOR_KEYS
if mode in ("minor", -1):
keylist = MINOR_KEYS
suffix = "m"
elif mode in ("major", None, "none", 1):
keylist = MAJOR_KEYS
suffix = ""
else:
raise Exception("Unknown mode {}".format(mode))
try:
name = keylist[fifths + 7]
except IndexError:
raise Exception("Unknown number of fifths {}".format(fifths))
return name + suffix
[docs]def key_name_to_fifths_mode(key_name):
"""Return the number of sharps or flats and the mode of a key
signature name. A negative number denotes the number of flats
(i.e. -3 means three flats), and a positive number the number of
sharps. The mode is specified as 'major' or 'minor'.
Parameters
----------
name : str
Name of the key signature, i.e. Am, E#, etc
Returns
-------
(int, str)
Tuple containing the number of fifths and the mode
Examples
--------
>>> key_name_to_fifths_mode('Am')
(0, 'minor')
>>> key_name_to_fifths_mode('C')
(0, 'major')
>>> key_name_to_fifths_mode('A')
(3, 'major')
"""
fifths_list = ["F", "C", "G", "D", "A", "E", "B"]
if "m" in key_name:
mode = "minor"
s_list = fifths_list[4:] + fifths_list[:4]
if "b" in key_name or (len(key_name) == 2 and s_list.index(key_name[0]) > 2):
idx = s_list[::-1].index(key_name[0]) + 1
corr = 1 if idx > 4 else 0
fifths = -idx - 7 * (key_name.count("b") - corr)
else:
idx = s_list.index(key_name[0])
corr = 1 if idx > 2 else 0
fifths = idx + 7 * (key_name.count("#") - corr)
else:
mode = "major"
s_list = fifths_list[1:] + fifths_list[:1]
if "b" in key_name or key_name == "F":
idx = s_list[::-1].index(key_name[0]) + 1
corr = 1 if idx > 1 else 0
fifths = -idx - 7 * (key_name.count("b") - corr)
else:
idx = s_list.index(key_name[0])
corr = 1 if idx > 5 else 0
fifths = idx + 7 * (key_name.count("#") - corr)
return fifths, mode
[docs]def key_mode_to_int(mode):
"""Return the mode of a key as an integer (1 for major and -1 for
minor).
Parameters
----------
mode : {'major', 'minor', None, 1, -1}
Mode of the key
Returns
-------
int
Integer representation of the mode.
"""
if mode in ("minor", -1):
return -1
elif mode in ("major", None, 1):
return 1
else:
raise ValueError("Unknown mode {}".format(mode))
def key_int_to_mode(mode):
"""Return the mode of a key as a string ('major' or 'minor')
Parameters
----------
mode : {'major', 'minor', None, 1, -1}
Mode of the key
Returns
-------
int
Integer representation of the mode.
"""
if mode in ("minor", -1):
return "minor"
elif mode in ("major", None, 1):
return "major"
else:
raise ValueError("Unknown mode {}".format(mode))
def estimate_symbolic_duration(dur, div, eps=10**-3):
"""Given a numeric duration, a divisions value (specifiying the
number of units per quarter note) and optionally a tolerance `eps`
for numerical imprecisions, estimate corresponding the symbolic
duration. If a matching symbolic duration is found, it is returned
as a tuple (type, dots), where type is a string such as 'quarter',
or '16th', and dots is an integer specifying the number of dots.
If no matching symbolic duration is found the function returns
None.
NOTE : this function does not estimate composite durations, nor
time-modifications such as triplets.
Parameters
----------
dur : float or int
Numeric duration value
div : int
Number of units per quarter note
eps : float, optional (default: 10**-3)
Tolerance in case of imprecise matches
Returns
-------
Examples
--------
>>> estimate_symbolic_duration(24, 16)
{'type': 'quarter', 'dots': 1}
>>> estimate_symbolic_duration(15, 10)
{'type': 'quarter', 'dots': 1}
The following example returns None:
>>> estimate_symbolic_duration(23, 16)
"""
global DURS, SYM_DURS
qdur = dur / div
i = find_nearest(DURS, qdur)
if np.abs(qdur - DURS[i]) < eps:
return SYM_DURS[i].copy()
else:
return None
def to_quarter_tempo(unit, tempo):
"""Given a string `unit` (e.g. 'q', 'q.' or 'h') and a number
`tempo`, return the corresponding tempo in quarter notes. This is
useful to convert textual tempo directions like h=100.
Parameters
----------
unit : str
Tempo unit
tempo : number
Tempo value
Returns
-------
float
Tempo value in quarter units
Examples
--------
>>> to_quarter_tempo('q', 100)
100.0
>>> to_quarter_tempo('h', 100)
200.0
>>> to_quarter_tempo('h.', 50)
150.0
"""
dots = unit.count(".")
unit = unit.strip().rstrip(".")
return float(tempo * DOT_MULTIPLIERS[dots] * LABEL_DURS[unit])
def format_symbolic_duration(symbolic_dur):
"""Create a string representation of the symbolic duration encoded
in the dictionary `symbolic_dur`.
Parameters
----------
symbolic_dur : dict
Dictionary with keys 'type' and 'dots'
Returns
-------
str
A string representation of the specified symbolic duration
Examples
--------
>>> format_symbolic_duration({'type': 'q', 'dots': 2})
'q..'
>>> format_symbolic_duration({'type': '16th'})
'16th'
"""
if symbolic_dur is None:
return "unknown"
else:
result = (symbolic_dur.get("type") or "") + "." * symbolic_dur.get("dots", 0)
if "actual_notes" in symbolic_dur and "normal_notes" in symbolic_dur:
result += "_{}/{}".format(
symbolic_dur["actual_notes"], symbolic_dur["normal_notes"]
)
return result
def symbolic_to_numeric_duration(symbolic_dur, divs):
numdur = divs * LABEL_DURS[symbolic_dur.get("type", None)]
numdur *= DOT_MULTIPLIERS[symbolic_dur.get("dots", 0)]
numdur *= (symbolic_dur.get("normal_notes") or 1) / (
symbolic_dur.get("actual_notes") or 1
)
return numdur
def order_splits(start, end, smallest_unit):
"""Description
Parameters
----------
start : int
Description of `start`
end : int
Description of `end`
smallest_divs : int
Description of `smallest_divs`
Returns
-------
ndarray
Description of return value
Examples
--------
>>> order_splits(1, 8, 1)
array([4, 2, 6, 3, 5, 7])
>>> order_splits(11, 17, 3)
array([12, 15])
>>> order_splits(11, 17, 1)
array([16, 12, 14, 13, 15])
>>> order_splits(11, 17, 4)
array([16, 12])
"""
# gegeven b, kies alle veelvouden van 2*b, verschoven om b,
# die tussen start en end liggen
# gegeven b, kies alle veelvouden van 2*b die tussen start-b
# en end-b liggen en tel er b bij op
b = smallest_unit
result = []
splits = np.arange((b * 2) * (1 + (start + b) // (b * 2)), end + b, b * 2) - b
while b * (1 + start // b) < end and b * (end // b) > start:
result.insert(0, splits)
b = b * 2
splits = np.arange((b * 2) * (1 + (start + b) // (b * 2)), end + b, b * 2) - b
if result:
return np.concatenate(result)
else:
return np.array([])
def find_smallest_unit(divs):
unit = divs
while unit % 2 == 0:
unit = unit // 2
return unit
def find_tie_split(start, end, divs, max_splits=3):
"""
Examples
--------
>>> find_tie_split(1, 8, 2)
[(1, 8, {'type': 'half', 'dots': 2})]
>>> find_tie_split(0, 3615, 480) # doctest: +NORMALIZE_WHITESPACE
[(0, 3600, {'type': 'whole', 'dots': 3}),
(3600, 3615, {'type': '128th', 'dots': 0})]
"""
smallest_unit = find_smallest_unit(divs)
def success(state):
return all(
estimate_symbolic_duration(right - left, divs)
for left, right in iter_current_next([start] + state + [end])
)
def expand(state):
if len(state) >= max_splits:
return []
else:
split_start = ([start] + state)[-1]
ordered_splits = order_splits(split_start, end, smallest_unit)
new_states = [state + [s.item()] for s in ordered_splits]
# start and end must be "in sync" with splits for states to succeed
new_states = [
s
for s in new_states
if (s[0] - start) % smallest_unit == 0
and (end - s[-1]) % smallest_unit == 0
]
return new_states
def combine(new_states, old_states):
return old_states + new_states
states = [[]]
# splits = search_recursive(states, success, expand, combine)
splits = search(states, success, expand, combine)
if splits is not None:
solution = [
(left, right, estimate_symbolic_duration(right - left, divs))
for left, right in iter_current_next([start] + splits + [end])
]
# print(solution)
return solution
else:
pass # print('no solution for ', start, end, divs)
def estimate_clef_properties(pitches):
# estimate the optimal clef for the given pitches. This returns a dictionary
# with the sign, line, and octave_change attributes of the clef
# (cf. MusicXML clef description). Currently only G and F clefs without
# octave changes are supported.
center = np.median(pitches)
# number, sign, line, octave_change):
# clefs = [score.Clef(1, 'F', 4, 0), score.Clef(1, 'G', 2, 0)]
clefs = [
dict(sign="F", line=4, octave_change=0),
dict(sign="G", line=2, octave_change=0),
]
f = interp1d([0, 49, 70, 127], [0, 0, 1, 1], kind="nearest")
return clefs[int(f(center))]
[docs]def compute_pianoroll(
note_info: Union[np.ndarray, ScoreLike, PerformanceLike],
time_unit: str = "auto",
time_div: Union[str, int] = "auto",
onset_only: bool = False,
note_separation: bool = False,
pitch_margin: int = -1,
time_margin: int = 0,
return_idxs: bool = False,
piano_range: bool = False,
remove_drums: bool = True,
remove_silence: bool = True,
end_time: Optional[int] = None,
binary: bool = False,
):
"""
Computes a piano roll from a score-like, performance-like or a
note array.
A piano roll is a 2D matrix of size (`pitch_range`, `num_time_steps`), where each
row represents a MIDI pitch and each column represents a time step. The (i,j)-th
element specifies whether pitch i is active (i.e., non-zero) at time step j.
The `pitch_range` is specified by the parameters `piano_range` and `pitch_margin`,
(see below), but it defaults to 128 (the standard range of MIDI note numbers),
or 88 if `piano_range` is True. The `num_time_steps` are specified by the temporal
resolution of the piano roll and the length of the piece, and can be controlled
with parameters `time_div`, `time_unit` and `time_margin` below.
Parameters
----------
note_info : np.ndarray, ScoreLike, PerformanceLike
Note information
time_unit : ('auto', 'beat', 'quarter', 'div', 'sec')
The time unit to use for computing the piano roll. If "auto",
the time unit defaults to "beat" for score-like objects and
"sec" for performance-like objects.
time_div : int, optional
How many sub-divisions for each time unit (beats for a score
or seconds for a performance. See `is_performance` below).
onset_only : bool, optional
If True, code only the onsets of the notes, otherwise code
onset and duration.
pitch_margin : int, optional
If `pitch_margin` > -1, the resulting array will have
`pitch_margin` empty rows above and below the highest and
lowest pitches, respectively; if `pitch_margin` == -1, the
resulting pianoroll will have span the fixed pitch range
between (and including) 1 and 127.
time_margin : int, optional
The resulting array will have `time_margin` * `time_div` empty
columns before and after the piano roll
return_idxs : bool, optional
If True, return the indices (i.e., the coordinates) of each
note in the piano roll.
piano_range : bool, optional
If True, the pitch axis of the piano roll is in piano keys
instead of MIDI note numbers (and there are only 88 pitches).
This is equivalent as slicing `piano_range_pianoroll =
pianoroll[21:109, :]`.
remove_drums : bool, optional
If True, removes the drum track (i.e., channel 9) from the
notes to be considered in the piano roll. This option is only
relevant for piano rolls generated from a `PerformedPart`.
Default is True.
remove_silence : bool, optional
If True, the first frame of the pianoroll starts at the onset
of the first note, not at time 0 of the timeline.
end_time : int, optional
The time corresponding to the ending of the last
pianoroll frame (in time_unit).
If None this is set to the last note offset.
binary: bool, optional
Ensure a strictly binary piano roll.
Returns
-------
pianoroll : scipy.sparse.csr_matrix
A sparse int matrix of size representing the pianoroll; The
first dimension is pitch, the second is time; The sizes of the
dimensions vary with the parameters `pitch_margin`,
`time_margin`, `time_div`, `remove silence`, and `end_time`.
pr_idx : ndarray
Indices of the onsets and offsets of the notes in the piano
roll (in the same order as the input note_array). This is only`
returned if `return_idxs` is True. The indices have 4 columns
(`vertical_position_in_piano_roll`, `onset`, `offset`, `original_midi_pitch`).
The `vertical_position_in_piano_roll` might be different from
`original_midi_pitch` depending on the `pitch_margin` and `piano_range`
arguments.
Examples
--------
>>> import numpy as np
>>> from partitura.utils import compute_pianoroll
>>> note_array = np.array([(60, 0, 1)],\
dtype=[('pitch', 'i4'),\
('onset_beat', 'f4'),\
('duration_beat', 'f4')])
>>> pr = compute_pianoroll(note_array, pitch_margin=2, time_div=2)
>>> pr.toarray()
array([[0, 0],
[0, 0],
[1, 1],
[0, 0],
[0, 0]])
Notes
-----
The default values in this function assume that the input
`note_array` represents a score.
"""
note_array = ensure_notearray(note_info)
if time_unit not in TIME_UNITS + ["auto"]:
raise ValueError(
"`time_unit` must be one of "
'{0} or "auto", but was given '
"{1}".format(", ".join(TIME_UNITS), time_unit)
)
if time_unit == "auto":
onset_unit, duration_unit = get_time_units_from_note_array(note_array)
else:
onset_unit = f"onset_{time_unit}"
duration_unit = f"duration_{time_unit}"
if time_div == "auto":
if onset_unit in ("onset_beat", "onset_quarter", "onset_sec"):
time_div = 8
elif onset_unit == "onset_div":
time_div = 1
else:
time_div = int(time_div)
if "channel" in note_array.dtype.names and remove_drums:
warnings.warn("Do not consider drum track for computing piano roll")
non_drum_idxs = np.where(note_array["channel"] != 9)[0]
note_array = note_array[non_drum_idxs]
piano_roll_fields = ["pitch", onset_unit, duration_unit]
if "velocity" in note_array.dtype.names:
piano_roll_fields += ["velocity"]
pr_input = np.column_stack(
[note_array[field].astype(float) for field in piano_roll_fields]
)
return _make_pianoroll(
note_info=pr_input,
time_div=time_div,
onset_only=onset_only,
note_separation=note_separation,
pitch_margin=pitch_margin,
time_margin=time_margin,
return_idxs=return_idxs,
piano_range=piano_range,
remove_silence=remove_silence,
end_time=end_time,
binary=binary,
)
def _make_pianoroll(
note_info: np.ndarray,
onset_only: bool = False,
pitch_margin: int = -1,
time_margin: int = 0,
time_div: int = 8,
note_separation: bool = True,
return_idxs: bool = False,
piano_range: bool = False,
remove_silence: bool = True,
min_time: Optional[float] = None,
end_time: Optional[int] = None,
binary: bool = False,
):
# non-public
"""
Computes a piano roll from a numpy array with MIDI pitch,
onset, duration and (optionally) MIDI velocity information. See
`compute_pianoroll` for a complete description of the
arguments of this function.
"""
# Get pitch, onset, offset from the note_info array
pr_pitch = note_info[:, 0]
onset = note_info[:, 1]
duration = note_info[:, 2]
if np.any(duration < 0):
raise ValueError("Note durations should be >= 0!")
# Get velocity if given
if note_info.shape[1] < 4:
pr_velocity = np.ones(len(note_info))
else:
pr_velocity = note_info[:, 3]
# Adjust pitch margin
if pitch_margin > -1:
highest_pitch = np.max(pr_pitch)
lowest_pitch = np.min(pr_pitch)
else:
lowest_pitch = 0
highest_pitch = 127
pitch_span = highest_pitch - lowest_pitch + 1
# sorted idx
idx = np.argsort(onset)
# sort notes
pr_pitch = pr_pitch[idx]
onset = onset[idx]
duration = duration[idx]
if min_time is None:
min_time = 0 if min(onset) >= 0 else min(onset)
if remove_silence:
min_time = onset[0]
else:
if min_time > min(onset):
raise ValueError(
"`min_time` must be smaller or equal than " "the smallest onset time "
)
onset -= min_time
if end_time is not None:
end_time -= min_time
if pitch_margin > -1:
pr_pitch -= lowest_pitch
pr_pitch += pitch_margin
# Size of the output piano roll
# Pitch dimension
if pitch_margin > -1:
M = int(pitch_span + 2 * pitch_margin)
else:
M = int(pitch_span)
# Onset and offset times of the notes in the piano roll
pr_onset = np.round(time_div * onset).astype(int)
pr_onset += int(time_margin * time_div)
pr_duration = np.clip(
np.round(time_div * duration).astype(int), a_max=None, a_min=1
)
pr_offset = pr_onset + pr_duration
# Time dimension
if end_time is None:
N = int(np.ceil(time_div * time_margin + pr_offset.max()))
else:
if end_time * time_div < pr_offset.max():
raise ValueError(
"`end_time` must be higher or equal than the last note offset time"
)
else:
N = int(np.ceil(time_div * time_margin + time_div * end_time))
# Determine the non-zero indices of the piano roll
if onset_only:
_idx_fill = np.column_stack([pr_pitch, pr_onset, pr_velocity])
else:
pr_offset = np.maximum(pr_onset + 1, pr_offset - (1 if note_separation else 0))
_idx_fill = np.vstack(
[
np.column_stack(
(
np.zeros(off - on) + pitch,
np.arange(on, off),
np.zeros(off - on) + vel,
)
)
for on, off, pitch, vel in zip(
pr_onset, pr_offset, pr_pitch, pr_velocity
)
]
)
# Fix multiple notes with the same pitch and onset
fill_dict = defaultdict(list)
for row, col, vel in _idx_fill:
key = (int(row), int(col))
fill_dict[key].append(vel)
idx_fill = np.zeros((len(fill_dict), 3))
for i, ((row, column), vel) in enumerate(fill_dict.items()):
idx_fill[i] = np.array([row, column, max(vel)])
if binary:
# binarize piano roll
idx_fill[idx_fill[:, 2] != 0, 2] = 1
# Fill piano roll
pianoroll = csc_matrix(
(idx_fill[:, 2], (idx_fill[:, 0], idx_fill[:, 1])), shape=(M, N), dtype=int
)
pr_idx_pitch_start = 0
if piano_range:
pianoroll = pianoroll[21:109, :]
pr_idx_pitch_start = 21
if return_idxs:
# indices of each note in the piano roll
pr_idx = np.column_stack(
[pr_pitch - pr_idx_pitch_start, pr_onset, pr_offset, note_info[idx, 0]]
).astype(int)
return pianoroll, pr_idx[idx.argsort()]
else:
return pianoroll
[docs]def compute_pitch_class_pianoroll(
note_info: Union[ScoreLike, PerformanceLike, np.ndarray],
normalize: bool = True,
time_unit: str = "auto",
time_div: int = "auto",
onset_only: bool = False,
note_separation: bool = False,
time_margin: int = 0,
return_idxs: int = False,
remove_silence: bool = True,
end_time: Optional[float] = None,
binary: bool = False,
) -> np.ndarray:
"""
Compute a pitch class piano roll from a score-like or performance-like objects, or
from a note array as a structured numpy array.
A pitch class piano roll is a 2D matrix of size (12, num_time_steps), where each
row represents a pitch class (C=0, C#=1, D=2, etc.) and each column represents a
time step. The (i,j)-th element specifies whether pitch class i is active at time
step j.
See `compute_pianoroll` for more details.
Parameters
----------
note_info : np.ndarray, ScoreLike, PerformanceLike
Note information.
normalize: bool
Normalize the piano roll. If True, each slice (i.e., time-step)
will be normalized to sum to one. The resulting output is
a piano roll where each time step is the pitch class distribution.
time_unit : ('auto', 'beat', 'quarter', 'div', 'sec')
The time unit to use for computing the piano roll. If "auto",
the time unit defaults to "beat" for score-like objects and
"sec" for performance-like objects.
time_div : int, optional
How many sub-divisions for each time unit (beats for a score
or seconds for a performance. See `is_performance` below).
onset_only : bool, optional
If True, code only the onsets of the notes, otherwise code
onset and duration.
time_margin : int, optional
The resulting array will have `time_margin` * `time_div` empty
columns before and after the piano roll
return_idxs : bool, optional
If True, return the indices (i.e., the coordinates) of each
note in the piano roll.
piano_range : bool, optional
If True, the pitch axis of the piano roll is in piano keys
instead of MIDI note numbers (and there are only 88 pitches).
This is equivalent as slicing `piano_range_pianoroll =
pianoroll[21:109, :]`.
remove_drums : bool, optional
If True, removes the drum track (i.e., channel 9) from the
notes to be considered in the piano roll. This option is only
relevant for piano rolls generated from a `PerformedPart`.
Default is True.
remove_silence : bool, optional
If True, the first frame of the pianoroll starts at the onset
of the first note, not at time 0 of the timeline.
end_time : int, optional
The time corresponding to the ending of the last
pianoroll frame (in time_unit).
If None this is set to the last note offset.
binary: bool, optional
Ensure a strictly binary piano roll.
Returns
-------
pc_pianoroll : np.ndarray
The pitch class piano roll. The sizes of the
dimensions vary with the parameters `pitch_margin`,
`time_margin`, `time_div`, `remove silence`, and `end_time`.
pr_idx : ndarray
Indices of the onsets and offsets of the notes in the piano
roll (in the same order as the input note_array). This is only
returned if `return_idxs` is `True`. The indices have 4 columns
(pitch_class, onset, offset, original_midi_pitch).
"""
pianoroll = compute_pianoroll(
note_info=note_info,
time_unit=time_unit,
time_div=time_div,
onset_only=onset_only,
note_separation=note_separation,
pitch_margin=-1,
time_margin=time_margin,
return_idxs=return_idxs,
piano_range=False,
remove_drums=True,
remove_silence=remove_silence,
end_time=end_time,
binary=False,
)
if return_idxs:
pianoroll, pr_idxs = pianoroll
# update indices by converting MIDI pitch to pitch class
pr_idxs[:, 0] = np.mod(pr_idxs[:, 0], 12)
pc_pianoroll = np.zeros((12, pianoroll.shape[1]), dtype=float)
for i in range(int(np.ceil(128 / 12))):
pr_slice = pianoroll[i * 12 : (i + 1) * 12, :].toarray().astype(float)
pc_pianoroll[: pr_slice.shape[0], :] += pr_slice
if binary:
# only show active pitch classes
pc_pianoroll[pc_pianoroll > 0] = 1
if normalize:
norm_term = pc_pianoroll.sum(0)
# avoid dividing by 0 if a slice is empty
norm_term[np.isclose(norm_term, 0)] = 1
pc_pianoroll /= norm_term
if return_idxs:
return pc_pianoroll, pr_idxs
return pc_pianoroll
[docs]def pianoroll_to_notearray(pianoroll, time_div=8, time_unit="sec"):
"""Extract a structured note array from a piano roll.
For now, the structured note array is considered a
"performance".
Parameters
----------
pianoroll : array-like
2D array containing a piano roll. The first dimension is
pitch, and the second is time. The value of each "pixel" in
the piano roll is considered to be the MIDI velocity, and it
is supposed to be between 0 and 127.
time_div : int
How many sub-divisions for each time unit (see
`notearray_to_pianoroll`).
time_unit : {'beat', 'quarter', 'div', 'sec'}
time unit of the output note array.
Returns
-------
np.ndarray :
Structured array with pitch, onset, duration, velocity
and note id fields.
Notes
-----
Please note that all non-zero pixels will contribute to a note.
For the case of piano rolls with continuous values between 0 and 1
(as might be the case of those piano rolls produced using
probabilistic/generative models), we recomend to either 1) hard-
threshold the piano roll to have only 0s (note-off) or 1s (note-
on) or, 2) soft-threshold the notes (values below a certain
threshold are considered as not active and scale the active notes
to lie between 1 and 127).
"""
# check size of the piano roll
init_pitch = 0
if pianoroll.shape[0] != 128:
if pianoroll.shape[0] == 88:
init_pitch = 21
else:
raise ValueError(
"The shape of the piano roll must be (128, n_time_steps) or"
f"(88, n_timesteps) but is {pianoroll.shape}"
)
active_notes = {}
note_list = []
for ts in range(pianoroll.shape[1]):
active = pianoroll[:, ts].nonzero()[0]
del_notes = []
for note in active_notes:
if note not in active:
del_notes.append(note)
for note in del_notes:
note_list.append(active_notes.pop(note))
for note in active:
vel = int(pianoroll[note, ts])
if note not in active_notes:
active_notes[note] = [note, vel, ts, ts + 1]
else:
if vel != active_notes[note][1]:
note_list.append(active_notes.pop(note))
active_notes[note] = [note, vel, ts, ts + 1]
else:
active_notes[note][-1] += 1
remaining_active_notes = list(active_notes.keys())
for note in remaining_active_notes:
# append any note left
note_list.append(active_notes.pop(note))
# Sort array lexicographically by onset, pitch, offset and velocity
note_list.sort(key=lambda x: (x[2], x[0], x[3], x[1]))
# Create note array
note_array = np.array(
[
(
p + init_pitch,
float(on) / time_div,
float(off - on) / time_div,
np.round(vel),
f"n{i}",
)
for i, (p, vel, on, off) in enumerate(note_list)
],
dtype=[
("pitch", "i4"),
(f"onset_{time_unit}", "f4"),
(f"duration_{time_unit}", "f4"),
("velocity", "i4"),
("id", "U256"),
],
)
return note_array
def match_note_arrays(
input_note_array,
target_note_array,
fields=None,
epsilon=0.01,
first_note_at_zero=False,
check_duration=True,
return_note_idxs=False,
):
"""Compute a greedy matching of the notes of two note_arrays based on
onset, pitch and (optionally) duration. Returns an array of matched
note_array indices and (optionally) an array of the corresponding
matched note indices.
Get an array of note_array indices of the notes from an input note
array corresponding to a reference note array.
Parameters
----------
input_note_array : structured array
Array containing performance/score information
target_note_arr : structured array
Array containing performance/score information, which which we
want to match the input.
fields : strings or tuple of strings
Field names to use for onset and duration in note_arrays.
If None defaults to beats or seconds, respectively.
epsilon : float
Epsilon for comparison of onset times.
first_note_at_zero : bool
If True, shifts the onsets of both note_arrays to start at 0.
Returns
-------
matched_idxs : np.ndarray
Indices of input_note_array corresponding to target_note_array
Notes
-----
This is a greedy method. This method is useful to compare the
*same performance* in different formats or versions (e.g., in a
match file and MIDI), or the *same score* (e.g., a MIDI file generated
from a MusicXML file). It will not produce meaningful results between a
score and a performance.
"""
input_note_array = ensure_notearray(input_note_array)
target_note_array = ensure_notearray(target_note_array)
if fields is not None:
if isinstance(fields, (list, tuple)):
onset_key, duration_key = fields
elif isinstance(fields, str):
onset_key = fields
duration_key = None
if duration_key is None and check_duration:
check_duration = False
else:
raise ValueError(
"`fields` should be a tuple or a string, but given " f"{type(fields)}"
)
else:
onset_key, duration_key = get_time_units_from_note_array(input_note_array)
onset_key_check, _ = get_time_units_from_note_array(target_note_array)
if onset_key_check != onset_key:
raise ValueError("Input and target arrays have different field names!")
if first_note_at_zero:
i_start = input_note_array[onset_key].min()
t_start = target_note_array[onset_key].min()
else:
i_start, t_start = (0, 0)
# sort indices
i_sort_idx = np.argsort(input_note_array[onset_key])
t_sort_idx = np.argsort(target_note_array[onset_key])
# get onset, pitch and duration information
i_onsets = input_note_array[onset_key][i_sort_idx] - i_start
i_pitch = input_note_array["pitch"][i_sort_idx]
t_onsets = target_note_array[onset_key][t_sort_idx] - t_start
t_pitch = target_note_array["pitch"][t_sort_idx]
if check_duration:
i_duration = input_note_array[duration_key][i_sort_idx]
t_duration = target_note_array[duration_key][t_sort_idx]
matched_idxs = []
matched_note_idxs = []
# dictionary of lists. For each index of the target, get a list of the
# corresponding indices in the input
matched_target = defaultdict(list)
# dictionary of lists. For each index of the input, get a list of the
# corresponding indices in the target
matched_input = defaultdict(list)
for t, (i, o, p) in enumerate(zip(t_sort_idx, t_onsets, t_pitch)):
# candidate onset idxs (between o - epsilon and o + epsilon)
coix = np.where(
np.logical_and(i_onsets >= o - epsilon, i_onsets <= o + epsilon)
)[0]
if len(coix) > 0:
# index of the note with the same pitch
cpix = np.where(i_pitch[coix] == p)[0]
if len(cpix) > 0:
m_idx = 0
# index of the note with the closest duration
if len(cpix) > 1 and check_duration:
m_idx = abs(i_duration[coix[cpix]] - t_duration[t]).argmin()
# match notes
input_idx = int(i_sort_idx[coix[cpix[m_idx]]])
target_idx = i
# matched_idxs.append((input_idx, target_idx))
matched_input[input_idx].append(target_idx)
matched_target[target_idx].append(input_idx)
matched_target_idxs = []
for inix, taix in matched_input.items():
if len(taix) > 1:
# For the case that there are multiple notes aligned to the input note
# get indices of the target notes if they have not yet been used
taix_to_consider = np.array(
[ti for ti in taix if ti not in matched_target_idxs], dtype=int
)
if len(taix_to_consider) > 0:
# If there are some indices to consider
candidate_notes = target_note_array[taix_to_consider]
if check_duration:
best_candidate_idx = (
candidate_notes[duration_key]
- input_note_array[inix][duration_key]
).argmin()
else:
# Take the first one if no other information is given
best_candidate_idx = 0
matched_idxs.append((inix, taix_to_consider[best_candidate_idx]))
matched_target_idxs.append(taix_to_consider[best_candidate_idx])
else:
matched_idxs.append((inix, taix[0]))
matched_target_idxs.append(taix[0])
matched_idxs = np.array(matched_idxs)
warnings.warn(
"Length of matched idxs: " "{0}".format(len(matched_idxs)), stacklevel=2
)
warnings.warn(
"Length of input note_array: " "{0}".format(len(input_note_array)), stacklevel=2
)
warnings.warn(
"Length of target note_array: " "{0}".format(len(target_note_array)),
stacklevel=2,
)
if return_note_idxs:
if len(matched_idxs) > 0:
matched_note_idxs = np.array(
[
input_note_array["id"][matched_idxs[:, 0]],
target_note_array["id"][matched_idxs[:, 1]],
]
).T
return matched_idxs, matched_note_idxs
else:
return matched_idxs
def remove_silence_from_performed_part(ppart):
"""
Remove silence at the beginning of a PerformedPart
by shifting notes, controls and programs to the beginning
of the file.
Parameters
----------
ppart : `PerformedPart`
A performed part. This part will be edited in-place.
"""
# Consider only Controls and Notes, since by default,
# programs are created at the beginning of the file.
# c_times = [c['time'] for c in ppart.controls]
n_times = [n["note_on"] for n in ppart.notes]
start_time = min(n_times)
shifted_controls = []
control_dict = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
for c in ppart.controls:
control_dict[c["track"]][c["channel"]][c["number"]].append(
(c["time"], c["value"])
)
for track in control_dict:
for channel in control_dict[track]:
for number, ct in control_dict[track][channel].items():
cta = np.array(ct)
cinterp = interp1d(
x=cta[:, 0],
y=cta[:, 1],
kind="previous",
bounds_error=False,
fill_value=(cta[0, 1], cta[-1, 1]),
)
c_idxs = np.where(cta[:, 0] >= start_time)[0]
c_times = cta[c_idxs, 0]
if start_time not in c_times:
c_times = np.r_[start_time, c_times]
c_values = cinterp(c_times)
for t, v in zip(c_times, c_values):
shifted_controls.append(
dict(
time=max(t - start_time, 0),
number=number,
value=int(v),
track=track,
channel=channel,
)
)
# sort controls according to time
shifted_controls.sort(key=lambda x: x["time"])
ppart.controls = shifted_controls
# Shift notes
for note in ppart.notes:
note["note_on"] = max(note["note_on"] - start_time, 0)
note["note_off"] = max(note["note_off"] - start_time, 0)
note["sound_off"] = max(note["sound_off"] - start_time, 0)
# Shift programs
for program in ppart.programs:
program["time"] = max(program["time"] - start_time, 0)
def note_array_from_part_list(
part_list,
unique_id_per_part=True,
**kwargs,
):
"""
Construct a structured Note array from a list of Part objects
Parameters
----------
part_list : list
A list of `Part` or `PerformedPart` objects. All elements in
the list must be of the same type (i.e., no mixing `Part`
and `PerformedPart` objects in the same list.
unique_id_per_part : bool (optional)
Indicate from which part do each note come from in the note ids. Default is True.
**kwargs : dict
Additional keyword arguments to pass to `utils.music.note_array_from_part()`
Returns
-------
note_array: structured array
A structured array containing pitch, onset, duration, voice
and id for each note in each part of the `part_list`. The note
ids in this array include the number of the part to which they
belong.
"""
from partitura.score import Part, PartGroup
from partitura.performance import PerformedPart
is_score = False
note_array = []
for i, part in enumerate(part_list):
if isinstance(part, (Part, PartGroup)):
# set include_divs_per_quarter, to correctly merge different divs
kwargs["include_divs_per_quarter"] = True
is_score = True
if isinstance(part, Part):
na = note_array_from_part(part, **kwargs)
elif isinstance(part, PartGroup):
na = note_array_from_part_list(
part.children, unique_id_per_part=unique_id_per_part, **kwargs
)
elif isinstance(part, PerformedPart):
na = part.note_array()
if unique_id_per_part and len(part_list) > 1:
# Update id with part number
na["id"] = np.array(
["P{0:02d}_".format(i) + nid for nid in na["id"]], dtype=na["id"].dtype
)
note_array.append(na)
if is_score:
# rescale if parts have different divs
divs_per_parts = [
part_na[0]["divs_pq"] for part_na in note_array if len(part_na)
]
lcm = np.lcm.reduce(divs_per_parts)
time_multiplier_per_part = [int(lcm / d) for d in divs_per_parts]
for na, time_mult in zip(note_array, time_multiplier_per_part):
na["onset_div"] = na["onset_div"] * time_mult
na["duration_div"] = na["duration_div"] * time_mult
na["divs_pq"] = na["divs_pq"] * time_mult
# concatenate note_arrays
note_array = np.hstack(note_array)
onset_unit, _ = get_time_units_from_note_array(note_array)
# sort by onset and pitch
pitch_sort_idx = np.argsort(note_array["pitch"])
note_array = note_array[pitch_sort_idx]
onset_sort_idx = np.argsort(note_array[onset_unit], kind="mergesort")
note_array = note_array[onset_sort_idx]
return note_array
def rest_array_from_part_list(
part_list,
unique_id_per_part=True,
include_pitch_spelling=False,
include_key_signature=False,
include_time_signature=False,
include_grace_notes=False,
include_staff=False,
collapse=False,
):
"""
Construct a structured Rest array from a list of Part objects
Parameters
----------
part_list : list
A list of `Part` or `PerformedPart` objects. All elements in
the list must be of the same type.
unique_id_per_part : bool (optional)
Indicate from which part do each rest come from in the rest ids.
include_pitch_spelling: bool (optional)
Include pitch spelling information in rest array.
This is a dummy attribute and returns zeros everywhere.
Default is False.
include_key_signature: bool (optional)
Include key signature information in output rest array.
Only valid if parts in `part_list` are `Part` objects.
See `rest_array_from_part` for more info.
Default is False.
include_time_signature : bool (optional)
Include time signature information in output rest array.
Only valid if parts in `part_list` are `Part` objects.
See `rest_array_from_part` for more info.
Default is False.
include_grace_notes : bool (optional)
If `True`, includes grace note information, i.e. "" for every rest).
Default is False
include_staff : bool (optional)
If `True`, includes note staff number.
Default is False
Returns
-------
rest_array: structured array
A structured array containing pitch (always zero), onset, duration, voice
and id for each rest in each part of the `part_list`. The rest
ids in this array include the number of the part to which they
belong.
"""
from partitura.score import Part, PartGroup
rest_array = []
for i, part in enumerate(part_list):
if isinstance(part, (Part, PartGroup)):
if isinstance(part, Part):
na = rest_array_from_part(
part=part,
unique_id_per_part=unique_id_per_part,
include_pitch_spelling=include_pitch_spelling,
include_key_signature=include_key_signature,
include_time_signature=include_time_signature,
include_grace_notes=include_grace_notes,
inlcude_staff=include_staff,
collapse=collapse,
)
elif isinstance(part, PartGroup):
na = rest_array_from_part_list(
part_list=part.children,
unique_id_per_part=unique_id_per_part,
include_pitch_spelling=include_pitch_spelling,
include_key_signature=include_key_signature,
include_time_signature=include_time_signature,
include_grace_notes=include_grace_notes,
inlcude_staff=include_staff,
collapse=collapse,
)
if unique_id_per_part:
# Update id with part number
na["id"] = np.array(
["P{0:02d}_".format(i) + nid for nid in na["id"]], dtype=na["id"].dtype
)
rest_array.append(na)
# concatenate note_arrays
rest_array = np.hstack(rest_array)
onset_unit, _ = get_time_units_from_note_array(rest_array)
# sort by onset and pitch
pitch_sort_idx = np.argsort(rest_array["pitch"])
rest_array = rest_array[pitch_sort_idx]
onset_sort_idx = np.argsort(rest_array[onset_unit], kind="mergesort")
rest_array = rest_array[onset_sort_idx]
return rest_array
[docs]def slice_notearray_by_time(
note_array, start_time, end_time, time_unit="auto", clip_onset_duration=True
):
"""
Get a slice of a structured note array by time
Parameters
----------
note_array : structured array
Structured array with score information.
start_time : float
Starting time
end_time : float
End time
time_unit : {'auto', 'beat', 'quarter', 'second', 'div'} optional
Time unit. If 'auto', the default time unit will be inferred
from the note_array.
clip_onset_duration : bool optional
Clip duration of the notes in the array to fit within the
specified window
Returns
-------
note_array_slice : stuctured array
Structured array with only the score information between
`start_time` and `end_time`.
TODO
----
* adjust onsets and duration in other units
"""
if time_unit not in TIME_UNITS + ["auto"]:
raise ValueError(
"`time_unit` must be 'beat', 'quarter', "
"'sec', 'div' or 'auto', but is "
"{0}".format(time_unit)
)
if time_unit == "auto":
onset_unit, duration_unit = get_time_units_from_note_array(note_array)
else:
onset_unit, duration_unit = [
"{0}_{1}".format(d, time_unit) for d in ("onset", "duration")
]
onsets = note_array[onset_unit]
offsets = note_array[onset_unit] + note_array[duration_unit]
starting_idxs = set(np.where(onsets >= start_time)[0])
ending_idxs = set(np.where(onsets < end_time)[0])
prev_starting_idxs = set(np.where(onsets < start_time)[0])
sounding_after_start_idxs = set(np.where(offsets > start_time)[0])
active_idx = np.array(
list(
starting_idxs.intersection(ending_idxs).union(
prev_starting_idxs.intersection(sounding_after_start_idxs)
)
)
)
active_idx.sort()
if len(active_idx) == 0:
# If there are no elements, return an empty array
note_array_slice = np.empty(0, dtype=note_array.dtype)
else:
note_array_slice = note_array[active_idx]
if clip_onset_duration and len(active_idx) > 0:
psi = np.where(note_array_slice[onset_unit] < start_time)[0]
note_array_slice[psi] = start_time
adj_offsets = np.clip(
note_array_slice[onset_unit] + note_array_slice[duration_unit],
a_min=None,
a_max=end_time,
)
note_array_slice[duration_unit] = adj_offsets - note_array_slice[onset_unit]
return note_array_slice
[docs]def note_array_from_part(
part,
include_pitch_spelling=False,
include_key_signature=False,
include_time_signature=False,
include_metrical_position=False,
include_grace_notes=False,
include_staff=False,
include_divs_per_quarter=False,
):
"""
Create a structured array with note information
from a `Part` object.
Parameters
----------
part : partitura.score.Part
An object representing a score part.
include_pitch_spelling : bool (optional)
It's a dummy attribute for consistancy between note_array_from_part and note_array_from_part_list.
Default is False
include_pitch_spelling : bool (optional)
If `True`, includes pitch spelling information for each
note. Default is False
include_key_signature : bool (optional)
If `True`, includes key signature information, i.e.,
the key signature at the onset time of each note (all
notes starting at the same time have the same key signature).
Default is False
include_time_signature : bool (optional)
If `True`, includes time signature information, i.e.,
the time signature at the onset time of each note (all
notes starting at the same time have the same time signature).
Default is False
include_metrical_position : bool (optional)
If `True`, includes metrical position information, i.e.,
the position of the onset time of each note with respect to its
measure (all notes starting at the same time have the same metrical
position).
Default is False
include_grace_notes : bool (optional)
If `True`, includes grace note information, i.e. if a note is a
grace note and the grace type "" for non grace notes).
Default is False
include_staff : bool (optional)
If `True`, includes staff information
Default is False
include_divs_per_quarter : bool (optional)
If `True`, include the number of divs (e.g. MIDI ticks,
MusicXML ppq) per quarter note of the current part.
Default is False
Returns
-------
note_array : structured array
A structured array containing note information. The fields are
* 'onset_beat': onset time of the note in beats
* 'duration_beat': duration of the note in beats
* 'onset_quarter': onset time of the note in quarters
* 'duration_quarter': duration of the note in quarters
* 'onset_div': onset of the note in divs (e.g., MIDI ticks,
divisions in MusicXML)
* 'duration_div': duration of the note in divs
* 'pitch': MIDI pitch of a note.
* 'voice': Voice number of a note (if given in the score)
* 'id': Id of the note
If `include_pitch_spelling` is True:
* 'step': name of the note ("C", "D", "E", "F", "G", "A", "B")
* 'alter': alteration (0=natural, -1=flat, 1=sharp,
2=double sharp, etc.)
* 'octave': octave of the note.
If `include_key_signature` is True:
* 'ks_fifths': Fifths starting from C in the circle of fifths
* 'mode': major or minor
If `include_time_signature` is True:
* 'ts_beats': number of beats in a measure
* 'ts_beat_type': type of beats (denominator of the time signature)
* 'ts_mus_beat' : number of musical beats is it's set, otherwise ts_beats
If `include_metrical_position` is True:
* 'is_downbeat': 1 if the note onset is on a downbeat, 0 otherwise
* 'rel_onset_div': number of divs elapsed from the beginning of the note measure
* 'tot_measure_divs' : total number of divs in the note measure
If 'include_grace_notes' is True:
* 'is_grace': 1 if the note is a grace 0 otherwise
* 'grace_type' : the type of the grace notes "" for non grace notes
If 'include_staff' is True:
* 'staff' : the staff number for each note
If 'include_divs_per_quarter' is True:
* 'divs_pq': the number of divs per quarter note
Examples
--------
>>> from partitura import load_musicxml, EXAMPLE_MUSICXML
>>> from partitura.utils import note_array_from_part
>>> part = load_musicxml(EXAMPLE_MUSICXML)
>>> note_array_from_part(part, True, True, True) # doctest: +NORMALIZE_WHITESPACE
array([(0., 4., 0., 4., 0, 48, 69, 1, 'n01', 'A', 0, 4, 0, 1, 4, 4),
(2., 2., 2., 2., 24, 24, 72, 2, 'n02', 'C', 0, 5, 0, 1, 4, 4),
(2., 2., 2., 2., 24, 24, 76, 2, 'n03', 'E', 0, 5, 0, 1, 4, 4)],
dtype=[('onset_beat', '<f4'),
('duration_beat', '<f4'),
('onset_quarter', '<f4'),
('duration_quarter', '<f4'),
('onset_div', '<i4'),
('duration_div', '<i4'),
('pitch', '<i4'),
('voice', '<i4'),
('id', '<U256'),
('step', '<U256'),
('alter', '<i4'),
('octave', '<i4'),
('ks_fifths', '<i4'),
('ks_mode', '<i4'),
('ts_beats', '<i4'),
('ts_beat_type', '<i4')])
"""
if include_time_signature:
time_signature_map = part.time_signature_map
else:
time_signature_map = None
if include_key_signature:
key_signature_map = part.key_signature_map
else:
key_signature_map = None
if include_metrical_position:
metrical_position_map = part.metrical_position_map
else:
metrical_position_map = None
if include_divs_per_quarter:
parts_quarter_times = part._quarter_times
parts_quarter_durations = part._quarter_durations
if not len(parts_quarter_durations) == 1:
raise Exception(
"Note array from parts with multiple divisions is not supported. Found divisions",
parts_quarter_durations,
"at times",
parts_quarter_times,
)
divs_per_quarter = parts_quarter_durations[0]
else:
divs_per_quarter = None
note_array = note_array_from_note_list(
note_list=part.notes_tied,
beat_map=part.beat_map,
quarter_map=part.quarter_map,
time_signature_map=time_signature_map,
key_signature_map=key_signature_map,
metrical_position_map=metrical_position_map,
include_pitch_spelling=include_pitch_spelling,
include_grace_notes=include_grace_notes,
include_staff=include_staff,
divs_per_quarter=divs_per_quarter,
)
return note_array
def rest_array_from_part(
part,
include_pitch_spelling=False,
include_key_signature=False,
include_time_signature=False,
include_metrical_position=False,
include_grace_notes=False,
include_staff=False,
collapse=False,
):
"""
Create a structured array with rest information
from a `Part` object Similar to note_array.
Parameters
----------
part : partitura.score.Part
An object representing a score part.
include_pitch_spelling : bool (optional)
It's a dummy attribute for consistancy between rest_array_from_part and rest_array_from_part_list.
Default is False
include_pitch_spelling : bool (optional)
If `True`, includes pitch spelling information for each
rest.
This is a dummy attribute returns zeros everywhere.
Default is False
include_key_signature : bool (optional)
If `True`, includes key signature information, i.e.,
the key signature at the onset time of each rest (all
notes starting at the same time have the same key signature).
Default is False
include_time_signature : bool (optional)
If `True`, includes time signature information, i.e.,
the time signature at the onset time of each rest (all
rests starting at the same time have the same time signature).
Default is False
include_metrical_position : bool (optional)
If `True`, includes metrical position information, i.e.,
the position of the onset time of each note with respect to its
measure.
Default is False
include_grace_notes : bool (optional)
If `True`, includes grace note information, i.e. the grace type is "" for all rests).
Default is False
collapse : bool (optional)
If 'True', collapses consecutive rest onsets on the same voice, to a single rest of their combined duration.
Default is False
Returns
-------
rest_array : structured array
A structured array containing rest information (pitch is always 0).
"""
if include_time_signature:
time_signature_map = part.time_signature_map
else:
time_signature_map = None
if include_key_signature:
key_signature_map = part.key_signature_map
else:
key_signature_map = None
if include_metrical_position:
metrical_position_map = part.metrical_position_map
else:
metrical_position_map = None
rest_array = rest_array_from_rest_list(
rest_list=part.rests,
beat_map=part.beat_map,
quarter_map=part.quarter_map,
time_signature_map=time_signature_map,
key_signature_map=key_signature_map,
metrical_position_map=metrical_position_map,
include_pitch_spelling=include_pitch_spelling,
include_grace_notes=include_grace_notes,
include_staff=include_staff,
collapse=collapse,
)
return rest_array
def note_array_from_note_list(
note_list,
beat_map=None,
quarter_map=None,
time_signature_map=None,
key_signature_map=None,
metrical_position_map=None,
include_pitch_spelling=False,
include_grace_notes=False,
include_staff=False,
divs_per_quarter=None,
):
"""
Create a structured array with note information
from a a list of `Note` objects.
Parameters
----------
note_list : list of `Note` objects
A list of `Note` objects containing score information.
beat_map : callable or None
A function that maps score time in divs to score time in beats.
If `None` is given, the output structured array will not
include this information.
quarter_map: callable or None
A function that maps score time in divs to score time in quarters.
If `None` is given, the output structured array will not
include this information.
time_signature_map: callable or None (optional)
A function that maps score time in divs to the time signature at
that time (in terms of number of beats and beat type).
If `None` is given, the output structured array will not
include this information.
key_signature_map: callable or None (optional)
A function that maps score time in divs to the key signature at
that time (in terms of fifths and mode).
If `None` is given, the output structured array will not
include this information.
metrical_position_map: callable or None (optional)
A function that maps score time in divs to the position in
the measure at that time.
If `None` is given, the output structured array will not
include the metrical position information.
include_pitch_spelling : bool (optional)
If `True`, includes pitch spelling information for each
note. Default is False
include_grace_notes : bool (optional)
If `True`, includes grace note information, i.e. if a note is a
grace note has one of the types "appoggiatura, acciaccatura, grace" and
the grace type "" for non grace notes).
Default is False
include_staff : bool (optional)
If `True`, includes the staff number for every note.
Default is False
divs_per_quarter : int or None (optional)
The number of divs (e.g. MIDI ticks, MusicXML ppq) per quarter
note of the current part.
Default is None
Returns
-------
note_array : structured array
A structured array containing note information. The fields are
* 'onset_beat': onset time of the note in beats.
Included if `beat_map` is not `None`.
* 'duration_beat': duration of the note in beats.
Included if `beat_map` is not `None`.
* 'onset_quarter': onset time of the note in quarters.
Included if `quarter_map` is not `None`.
* 'duration_quarter': duration of the note in quarters.
Included if `quarter_map` is not `None`.
* 'onset_div': onset of the note in divs (e.g., MIDI ticks,
divisions in MusicXML)
* 'duration_div': duration of the note in divs
* 'pitch': MIDI pitch of a note.
* 'voice': Voice number of a note (if given in the score)
* 'id': Id of the note
* 'step': name of the note ("C", "D", "E", "F", "G", "A", "B").
Included if `include_pitch_spelling` is `True`.
* 'alter': alteration (0=natural, -1=flat, 1=sharp,
2=double sharp, etc.). Included if `include_pitch_spelling`
is `True`.
* 'octave': octave of the note. Included if `include_pitch_spelling`
is `True`.
* 'is_grace' : Is the note a grace note. Yes if true.
* 'grace_type' : The type of grace note. "" for non grace notes.
* 'ks_fifths': Fifths starting from C in the circle of fifths.
Included if `key_signature_map` is not `None`.
* 'mode': major or minor. Included If `key_signature_map` is
not `None`.
* 'ts_beats': number of beats in a measure. If `time_signature_map`
is True.
* 'ts_beat_type': type of beats (denominator of the time signature).
If `include_time_signature` is True.
* 'is_downbeat': 1 if the note onset is on a downbeat, 0 otherwise.
If `measure_map` is not None.
* 'rel_onset_div': number of divs elapsed from the beginning of the
note measure. If `measure_map` is not None.
* 'tot_measure_div' : total number of divs in the note measure
If `measure_map` is not None.
* 'staff' : number of note staff.
* 'divs_pq' : number of parts per quarter note.
"""
fields = []
if beat_map is not None:
# Preserve the order of the fields
fields += [("onset_beat", "f4"), ("duration_beat", "f4")]
if quarter_map is not None:
fields += [("onset_quarter", "f4"), ("duration_quarter", "f4")]
fields += [
("onset_div", "i4"),
("duration_div", "i4"),
("pitch", "i4"),
("voice", "i4"),
("id", "U256"),
]
# fields for pitch spelling
if include_pitch_spelling:
fields += [("step", "U256"), ("alter", "i4"), ("octave", "i4")]
# fields for pitch spelling
if include_grace_notes:
fields += [("is_grace", "b"), ("grace_type", "U256")]
# fields for key signature
if key_signature_map is not None:
fields += [("ks_fifths", "i4"), ("ks_mode", "i4")]
# fields for time signature
if time_signature_map is not None:
fields += [("ts_beats", "i4"), ("ts_beat_type", "i4"), ("ts_mus_beats", "i4")]
# fields for metrical position
if metrical_position_map is not None:
fields += [
("is_downbeat", "i4"),
("rel_onset_div", "i4"),
("tot_measure_div", "i4"),
]
# field for staff
if include_staff:
fields += [("staff", "i4")]
# field for divs_pq
if divs_per_quarter:
fields += [("divs_pq", "i4")]
note_array = []
for note in note_list:
note_info = tuple()
note_on_div = note.start.t
note_off_div = note.start.t + note.duration_tied
note_dur_div = note_off_div - note_on_div
if beat_map is not None:
note_on_beat, note_off_beat = beat_map([note_on_div, note_off_div])
note_dur_beat = note_off_beat - note_on_beat
note_info += (note_on_beat, note_dur_beat)
if quarter_map is not None:
note_on_quarter, note_off_quarter = quarter_map([note_on_div, note_off_div])
note_dur_quarter = note_off_quarter - note_on_quarter
note_info += (note_on_quarter, note_dur_quarter)
note_info += (
note_on_div,
note_dur_div,
note.midi_pitch,
note.voice if note.voice is not None else -1,
note.id,
)
if include_pitch_spelling:
step = note.step
alter = note.alter if note.alter is not None else 0
octave = note.octave
note_info += (step, alter, octave)
if include_grace_notes:
is_grace = hasattr(note, "grace_type")
if is_grace:
grace_type = note.grace_type
else:
grace_type = ""
note_info += (is_grace, grace_type)
if key_signature_map is not None:
fifths, mode = key_signature_map(note.start.t)
note_info += (fifths, mode)
if time_signature_map is not None:
beats, beat_type, mus_beats = time_signature_map(note.start.t)
note_info += (beats, beat_type, mus_beats)
if metrical_position_map is not None:
rel_onset_div, tot_measure_div = metrical_position_map(note.start.t)
is_downbeat = 1 if rel_onset_div == 0 else 0
note_info += (is_downbeat, rel_onset_div, tot_measure_div)
if include_staff:
note_info += ((note.staff if note.staff else 0),)
if divs_per_quarter:
note_info += (divs_per_quarter,)
note_array.append(note_info)
note_array = np.array(note_array, dtype=fields)
# Sanitize voice information
no_voice_idx = np.where(note_array["voice"] == -1)[0]
try:
max_voice = note_array["voice"].max()
except ValueError: # raised if `note_array["voice"]` is empty.
note_array["voice"] = 0
max_voice = 0
note_array["voice"][no_voice_idx] = max_voice + 1
# sort by onset and pitch
onset_unit, _ = get_time_units_from_note_array(note_array)
pitch_sort_idx = np.argsort(note_array["pitch"])
note_array = note_array[pitch_sort_idx]
onset_sort_idx = np.argsort(note_array[onset_unit], kind="mergesort")
note_array = note_array[onset_sort_idx]
return note_array
def rest_array_from_rest_list(
rest_list,
beat_map=None,
quarter_map=None,
time_signature_map=None,
key_signature_map=None,
metrical_position_map=None,
include_pitch_spelling=False,
include_grace_notes=False,
include_staff=False,
collapse=False,
):
"""
Create a structured array with rest information
from a list of `Rest` objects.
Parameters
----------
rest_list : list of `Rest` objects
A list of `Rest` objects containing score information.
beat_map : callable or None
A function that maps score time in divs to score time in beats.
If `None` is given, the output structured array will not
include this information.
quarter_map: callable or None
A function that maps score time in divs to score time in quarters.
If `None` is given, the output structured array will not
include this information.
time_signature_map: callable or None (optional)
A function that maps score time in divs to the time signature at
that time (in terms of number of beats and beat type).
If `None` is given, the output structured array will not
include this information.
key_signature_map: callable or None (optional)
A function that maps score time in divs to the key signature at
that time (in terms of fifths and mode).
If `None` is given, the output structured array will not
include this information.
metrical_position_map: callable or None (optional)
A function that maps score time in divs to the position in
the measure at that time.
If `None` is given, the output structured array will not
include the metrical position information.
include_pitch_spelling : bool (optional)
If `True`, includes pitch spelling information for each
rest. This is a dummy attribute and returns zeros everywhere.
Default is False
include_grace_notes : bool (optional)
If `True`, includes grace note information, i.e. "" for all rests).
Default is False
include_staff : bool (optional)
If `True`, includes the staff number for every note.
Default is False
collapse : bool (optional)
If `True`, joins rests on consecutive onsets on the same voice and combines their durations.
Keeps the id of the first one.
Default is False
Returns
-------
rest_array : structured array
A structured array containing rest information. Pitch is set to 0.
"""
fields = []
if beat_map is not None:
# Preserve the order of the fields
fields += [("onset_beat", "f4"), ("duration_beat", "f4")]
if quarter_map is not None:
fields += [("onset_quarter", "f4"), ("duration_quarter", "f4")]
fields += [
("onset_div", "i4"),
("duration_div", "i4"),
("pitch", "i4"),
("voice", "i4"),
("id", "U256"),
]
# fields for pitch spelling
if include_pitch_spelling:
fields += [("step", "U256"), ("alter", "i4"), ("octave", "i4")]
# fields for pitch spelling
if include_grace_notes:
fields += [("is_grace", "b"), ("grace_type", "U256")]
# fields for key signature
if key_signature_map is not None:
fields += [("ks_fifths", "i4"), ("ks_mode", "i4")]
# fields for time signature
if time_signature_map is not None:
fields += [("ts_beats", "i4"), ("ts_beat_type", "i4")]
# fields for metrical position
if metrical_position_map is not None:
fields += [
("is_downbeat", "i4"),
("rel_onset_div", "i4"),
("tot_measure_div", "i4"),
]
# fields for staff
if include_staff:
fields += [("staff", "i4")]
rest_array = []
for rest in rest_list:
rest_info = tuple()
rest_on_div = rest.start.t
rest_off_div = rest.start.t + rest.duration_tied
rest_dur_div = rest_off_div - rest_on_div
if beat_map is not None:
note_on_beat, note_off_beat = beat_map([rest_on_div, rest_off_div])
note_dur_beat = note_off_beat - note_on_beat
rest_info += (note_on_beat, note_dur_beat)
if quarter_map is not None:
note_on_quarter, note_off_quarter = quarter_map([rest_on_div, rest_off_div])
note_dur_quarter = note_off_quarter - note_on_quarter
rest_info += (note_on_quarter, note_dur_quarter)
rest_info += (
rest_on_div,
rest_dur_div,
0,
rest.voice if rest.voice is not None else -1,
rest.id,
)
if include_pitch_spelling:
step = 0
alter = 0
octave = 0
rest_info += (step, alter, octave)
if include_grace_notes:
is_grace = hasattr(rest, "grace_type")
if is_grace:
grace_type = rest.grace_type
else:
grace_type = ""
rest_info += (is_grace, grace_type)
if key_signature_map is not None:
fifths, mode = key_signature_map(rest.start.t)
rest_info += (fifths, mode)
if time_signature_map is not None:
beats, beat_type = time_signature_map(rest.start.t)
rest_info += (beats, beat_type)
if metrical_position_map is not None:
rel_onset_div, tot_measure_div = metrical_position_map(rest.start.t)
is_downbeat = 1 if rel_onset_div == 0 else 0
rest_info += (is_downbeat, rel_onset_div, tot_measure_div)
if include_staff:
rest_info += ((rest.staff if rest.staff else 0),)
rest_array.append(rest_info)
rest_array = np.array(rest_array, dtype=fields)
# Sanitize voice information
if rest_list:
no_voice_idx = np.where(rest_array["voice"] == -1)[0]
max_voice = rest_array["voice"].max()
rest_array["voice"][no_voice_idx] = max_voice + 1
# sort by onset and pitch
onset_unit, _ = get_time_units_from_note_array(rest_array)
pitch_sort_idx = np.argsort(rest_array["pitch"])
rest_array = rest_array[pitch_sort_idx]
onset_sort_idx = np.argsort(rest_array[onset_unit], kind="mergesort")
rest_array = rest_array[onset_sort_idx]
if collapse:
rest_array = rec_collapse_rests(rest_array)
return rest_array
def collapse_rests(rest_array):
filter_idx = []
output_idx = []
for i, rest in enumerate(rest_array):
if i not in filter_idx:
idxs = np.where(
(rest_array["onset_beat"] == rest["onset_beat"] + rest["duration_beat"])
& (rest_array["voice"] == rest["voice"])
)[0]
for idx in idxs:
rest_array[i]["duration_beat"] = (
rest["duration_beat"] + rest_array[idx]["duration_beat"]
)
rest_array[i]["duration_div"] = (
rest["duration_div"] + rest_array[idx]["duration_div"]
)
filter_idx.append(idx)
output_idx.append(i)
return rest_array[output_idx], filter_idx
def rec_collapse_rests(rest_array):
cond = True
while cond:
rest_array, filter_idx = collapse_rests(rest_array)
cond = len(filter_idx) > 0
return rest_array
def update_note_ids_after_unfolding(part):
note_id_dict = defaultdict(list)
for n in part.notes:
note_id_dict[n.id].append(n)
for nid, notes in note_id_dict.items():
if nid is None:
continue
notes.sort(key=lambda x: x.start.t)
for i, note in enumerate(notes):
note.id = f"{note.id}-{i+1}"
def performance_from_part(part, bpm=100, velocity=64):
"""
Create a PerformedPart object from a Part object
Parameters
----------
part: Part
The part from which we want to generate a performed part
bpm : float, np.ndarray or callable
Beats per minute to generate the performance. If a the value is a float,
the performance will be generated with a constant tempo. If the value is
a np.ndarray, it has to be an array with two columns where the first
column is score time in beats and the second column is the tempo. If a
callable is given, the function is assumed to map score onsets in beats
to tempo values. Default is 100 bpm.
velocity: int, np.ndarray or callable
MIDI velocity of the performance. If a the value is an int, the
performance will be generated with a constant MIDI velocity. If the
value is a np.ndarray, it has to be an array with two columns where
the first column is score time in beats and the second column is the
MIDI velocity. If a callable is given, the function is assumed to map
score time in beats to MIDI velocity. Default is 64.
Returns
-------
ppart: PerformedPart
A PerformedPart object with the generated performance.
"""
from partitura.score import Part
from partitura.performance import PerformedPart
if not isinstance(part, Part):
raise ValueError(
"The input `part` must be a "
f"`partitura.score.Part` instance, not {type(part)}"
)
snote_array = part.note_array()
pnote_array = performance_notearray_from_score_notearray(
snote_array=snote_array, bpm=bpm, velocity=velocity
)
ppart = PerformedPart.from_note_array(pnote_array)
return ppart
def performance_notearray_from_score_notearray(
snote_array: np.ndarray,
bpm: Union[float, np.ndarray, Callable] = 100.0,
velocity: Union[int, np.ndarray, Callable] = 64,
) -> np.ndarray:
"""
Generate a performance note array from a score note array
Parameters
----------
snote_array : np.ndarray
A score note array.
bpm : float, np.ndarray or callable
Beats per minute to generate the performance. If a the value is a float,
the performance will be generated with a constant tempo. If the value is
a np.ndarray, it has to be an array with two columns where the first
column is score time in beats and the second column is the tempo. If a
callable is given, the function is assumed to map score onsets in beats
to tempo values. Default is 100 bpm.
velocity: int, np.ndarray or callable
MIDI velocity of the performance. If a the value is an int, the
performance will be generated with a constant MIDI velocity. If the
value is a np.ndarray, it has to be an array with two columns where
the first column is score time in beats and the second column is the
MIDI velocity. If a callable is given, the function is assumed to map
score time in beats to MIDI velocity. Default is 64.
Returns
-------
pnote_array : np.ndarray
A performance note array based on the score with the specified tempo
and velocity.
"""
ppart_fields = [
("onset_sec", "f4"),
("duration_sec", "f4"),
("pitch", "i4"),
("velocity", "i4"),
("track", "i4"),
("channel", "i4"),
("id", "U256"),
]
pnote_array = np.zeros(len(snote_array), dtype=ppart_fields)
if isinstance(velocity, np.ndarray):
if velocity.ndim == 2:
velocity_fun = interp1d(
x=velocity[:, 0],
y=velocity[:, 1],
kind="previous",
bounds_error=False,
fill_value=(velocity[0, 1], velocity[-1, 1]),
)
pnote_array["velocity"] = np.round(
velocity_fun(snote_array["onset_beat"]),
).astype(int)
else:
pnote_array["velocity"] = np.round(velocity).astype(int)
elif callable(velocity):
# The velocity parameter is a callable that returns a
# velocity value for each score onset
pnote_array["velocity"] = np.round(
velocity(snote_array["onset_beat"]),
).astype(int)
else:
pnote_array["velocity"] = int(velocity)
unique_onsets = np.unique(snote_array["onset_beat"])
# Cast as object to avoid warnings, but seems to work well
# in numpy version 1.20.1
unique_onset_idxs = np.array(
[np.where(snote_array["onset_beat"] == u)[0] for u in unique_onsets],
dtype=object,
)
iois = np.diff(unique_onsets)
if callable(bpm) or isinstance(bpm, np.ndarray):
if callable(bpm):
# bpm parameter is a callable that returns a bpm value
# for each score onset
bp = 60 / bpm(unique_onsets)
bp_duration = (
60 / bpm(snote_array["onset_beat"]) * snote_array["duration_beat"]
)
elif isinstance(bpm, np.ndarray):
if bpm.ndim != 2:
raise ValueError("`bpm` should be a 2D array")
bpm_fun = interp1d(
x=bpm[:, 0],
y=bpm[:, 1],
kind="previous",
bounds_error=False,
fill_value=(bpm[0, 1], bpm[-1, 1]),
)
bp = 60 / bpm_fun(unique_onsets)
bp_duration = (
60 / bpm_fun(snote_array["onset_beat"]) * snote_array["duration_beat"]
)
p_onsets = np.r_[0, np.cumsum(iois * bp[:-1])]
pnote_array["duration_sec"] = bp_duration * snote_array["duration_beat"]
else:
# convert bpm to beat period
bp = 60 / float(bpm)
p_onsets = np.r_[0, np.cumsum(iois * bp)]
pnote_array["duration_sec"] = bp * snote_array["duration_beat"]
pnote_array["pitch"] = snote_array["pitch"]
pnote_array["id"] = snote_array["id"]
for ix, on in zip(unique_onset_idxs, p_onsets):
# ix has to be cast as integer depending on the
# numpy version...
pnote_array["onset_sec"][ix.astype(int)] = on
return pnote_array
def generate_random_performance_note_array(
num_notes: int = 20,
rng: Union[int, np.random.RandomState] = np.random.RandomState(1984),
duration: float = 10,
max_note_duration: float = 2,
min_note_duration: float = 0.1,
max_velocity: int = 90,
min_velocity: int = 20,
return_performance: bool = False,
) -> Union[np.ndarray, Performance]:
"""
Create a random performance note array.
Parameters
----------
num_notes : int
Number of notes
rng : int or np.random.RandomState
State for the random number generator. If an integer is given
a new random number generator with that state will be created.
duration : float
Total duration of the note array in seconds. Default is 10.
max_note_duration : float
Maximum duration of a note in seconds. Note that since the durations
are randomly sampled from a uniform distribution, it could happen
that no notes actually have this duration.
min_note_duration: float
Minimum duration of a note in seconds. Note that since the durations
are randomly sampled from a uniform distribution, it could happen
that no notes actually have this duration.
max_velocity : int
Maximal MIDI velocity. Note that since the MIDI velocities
are randomly sampled from a uniform distribution, it could happen
that no notes actually have this velocity.
min_velocity : int
Maximal MIDI velocity. Note that since the MIDI velocities
are randomly sampled from a uniform distribution, it could happen
that no notes actually have this velocity.
return_performance : bool
If True, returns a `Performance` object.
Returns
-------
note_array or performance : np.ndarray or Performance
If `return_performance` is True, the output is a `Performance` instance.
Otherwise, it returns a structured note array with note information.
"""
# Generate a random piano roll
if isinstance(rng, int):
rng = np.random.RandomState(rng)
note_array = np.empty(
num_notes,
dtype=[
("pitch", "i4"),
("onset_sec", "f4"),
("duration_sec", "f4"),
("velocity", "i4"),
("id", "U256"),
],
)
if max_note_duration >= duration:
warnings.warn(
message=(
"`duration` is smaller than `max_note_duration`! "
"The `max_note_duration` has been adjusted to be equal to "
"`0.5 * duration`."
)
)
max_note_duration = 0.5 * duration
note_array["pitch"] = rng.randint(1, 128, num_notes)
note_duration = rng.uniform(
low=min_note_duration,
high=max_note_duration,
size=num_notes,
)
onset = rng.uniform(
low=0,
high=1,
size=num_notes,
)
# Onsets start at 0 and end at duration - the smalles note duration
onset = (duration - note_duration.min()) * (onset - onset.min()) / onset.max()
# Ensure that the offsets end at the specified duration.
offset = np.clip(
onset + note_duration,
a_min=min_note_duration,
a_max=duration,
)
note_array["duration_sec"] = offset - onset
sort_idxs = onset.argsort()
# Note ids are sorted by onset.
note_array["id"] = np.array([f"n{i}" for i in sort_idxs])
note_array["onset_sec"] = onset
note_array["velocity"] = rng.randint(
min_velocity,
max_velocity + 1,
num_notes,
)
if return_performance:
from partitura.performance import Performance, PerformedPart
performed_part = PerformedPart.from_note_array(note_array)
performance = Performance(performed_part, performer=str(rng))
return performance
return note_array
def slice_ppart_by_time(
ppart: PerformedPart,
start_time: float,
end_time: float,
clip_note_off: bool = True,
reindex_notes: bool = False,
) -> PerformedPart:
"""
Get a slice of a PeformedPart by time
Parameters
----------
ppart : `PerformedPart` object
start_time : float
Starting time in seconds
end_time : float
End time in seconds
clip_note_off : bool
Clip note_off time to end_time
reindex_notes : bool
Reindex notes in slice starting from n0
Returns
-------
ppart_slice : `PerformedPart` object
A copy of input ppart containing notes, programs and control
information only between `start_time` and `end_time` of ppart
"""
from partitura.performance import PerformedPart
if not isinstance(ppart, PerformedPart):
raise ValueError("Input is not an instance of PerformedPart!")
if start_time > end_time:
raise ValueError("Start time not less than end time!")
# create a new (empty) instance of a PerformedPart
# single dummy note added to be able to set sustain_pedal_threshold in __init__
# -> check `adjust_offsets_w_sustain` in partitura.performance
# ppart_slice = PerformedPart([{"note_on": 0, "note_off": 0, "pitch": 0}])
# get ppq if PerformedPart contains it,
# else skip time_tick info when e.g. created with 'load_performance_midi'
try:
ppq = ppart.ppq
except AttributeError:
ppq = None
controls_slice = []
if ppart.controls:
for cc in ppart.controls:
if cc["time"] >= start_time and cc["time"] <= end_time:
new_cc = cc.copy()
new_cc["time"] -= start_time
if ppq:
new_cc["time_tick"] = int(2 * ppq * cc["time"])
controls_slice.append(new_cc)
programs_slice = []
if ppart.programs:
for pr in ppart.programs:
if pr["time"] >= start_time and pr["time"] <= end_time:
new_pr = pr.copy()
new_pr["time"] -= start_time
if ppq:
new_pr["time_tick"] = int(2 * ppq * pr["time"])
programs_slice.append(new_pr)
notes_slice = []
note_id = 0
for note in ppart.notes:
# collect previous sounding notes at start_time
if note["note_on"] < start_time and note["note_off"] > start_time:
new_note = note.copy()
new_note["note_on"] = 0.0
if clip_note_off:
new_note["note_off"] = min(
note["note_off"] - start_time, end_time - start_time
)
else:
new_note["note_off"] = note["note_off"] - start_time
if ppq:
new_note["note_on_tick"] = 0
new_note["note_off_tick"] = int(2 * ppq * new_note["note_off"])
if reindex_notes:
new_note["id"] = f"n{note_id}"
note_id += 1
notes_slice.append(new_note)
# todo - combine both cases
if note["note_on"] >= start_time:
if note["note_on"] < end_time:
new_note = note.copy()
new_note["note_on"] -= start_time
if clip_note_off:
new_note["note_off"] = min(
note["note_off"] - start_time, end_time - start_time
)
else:
new_note["note_off"] = note["note_off"] - start_time
if ppq:
new_note["note_on_tick"] = int(2 * ppq * new_note["note_on"])
new_note["note_off_tick"] = int(2 * ppq * new_note["note_off"])
if reindex_notes:
new_note["id"] = "n" + str(note_id)
note_id += 1
notes_slice.append(new_note)
# assumes notes in list are sorted by onset time
else:
break
# Create slice PerformedPart
ppart_slice = PerformedPart(
notes=notes_slice, programs=programs_slice, controls=controls_slice, ppq=ppq
)
# set threshold property after creating notes list to update 'sound_offset' values
ppart_slice.sustain_pedal_threshold = ppart.sustain_pedal_threshold
if ppart.id:
ppart_slice.id = ppart.id + "_slice_{}s_to_{}s".format(start_time, end_time)
if ppart.part_name:
ppart_slice.part_name = ppart.part_name
return ppart_slice
def tokenize(
score_data: ScoreLike,
tokenizer: MIDITokenizer,
incomplete_bar_behaviour: str = "pad_bar",
):
"""
Tokenize a score using a tokenizer from miditok.
Parameters
----------
score_data : Score, list, Part, or PartGroup
The musical score to be saved. A :class:`partitura.score.Score` object,
a :class:`partitura.score.Part`, a :class:`partitura.score.PartGroup` or
a list of these.
tokenizer : MIDITokenizer
A tokenizer from miditok.
incomplete_bar_behaviour : str
How to handle incomplete bars at the beginning (pickup measures) and
during the score. Can be one of 'pad_bar', 'shift', or 'time_sig_change'.
See :func:`partitura.io.exportmidi.save_score_midi` for details.
Defaults to 'pad_bar'.
Returns
-------
ppart_slice : `Tokens` object
Tokens as produced by the miditok library.
"""
if miditok is None or miditoolkit is None:
raise ImportError(
"Miditok and miditoolkit must be installed for this function to work"
)
with TemporaryDirectory() as tmpdir:
temp_midi_path = os.path.join(tmpdir, "temp_midi.mid")
partitura.io.exportmidi.save_score_midi(
score_data,
out=temp_midi_path,
anacrusis_behavior=incomplete_bar_behaviour,
part_voice_assign_mode=4,
minimum_ppq=480,
)
midi = miditoolkit.MidiFile(temp_midi_path)
tokens = tokenizer(midi)
return tokens
if __name__ == "__main__":
import doctest
doctest.testmod()