Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Fingering Annotations and Markings. #403

Merged
merged 5 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions partitura/io/exportmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ def matchfile_from_alignment(
staff = getattr(snote, "staff", None)
ornaments = getattr(snote, "ornaments", None)
fermata = getattr(snote, "fermata", None)
technical = getattr(snote, "technical", None)

if voice is not None:
score_attributes_list.append(f"v{voice}")
Expand All @@ -328,6 +329,11 @@ def matchfile_from_alignment(
if fermata is not None:
score_attributes_list.append("fermata")

if technical is not None:
for tech_el in technical:
if isinstance(tech_el, score.Fingering):
score_attributes_list.append(f"fingering{tech_el.fingering}")

if isinstance(snote, score.GraceNote):
score_attributes_list.append("grace")

Expand Down
35 changes: 27 additions & 8 deletions partitura/io/exportmei.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import math
from collections import defaultdict
from lxml import etree
import lxml
import partitura.score as spt
from operator import itemgetter
from itertools import groupby
Expand Down Expand Up @@ -225,6 +226,7 @@ def _handle_measure(self, measure, measure_el):
self._handle_harmony(measure_el, start=measure.start.t, end=measure.end.t)
self._handle_fermata(measure_el, start=measure.start.t, end=measure.end.t)
self._handle_barline(measure_el, start=measure.start.t, end=measure.end.t)
self._handle_fingering(measure_el, start=measure.start.t, end=measure.end.t)
return measure_el

def _handle_chord(self, chord, xml_voice_el):
Expand Down Expand Up @@ -295,7 +297,7 @@ def _handle_note(self, note, xml_voice_el):
note_el.set("grace", "acc")
return duration

def _handle_tuplets(self, measure_el, start, end):
def _handle_tuplets(self, measure_el: lxml.etree._Element, start: int, end: int):
for tuplet in self.part.iter_all(spt.Tuplet, start=start, end=end):
start_note = tuplet.start_note
end_note = tuplet.end_note
Expand Down Expand Up @@ -352,7 +354,7 @@ def _handle_tuplets(self, measure_el, start, end):
for el in xml_el_within_tuplet:
tuplet_el.append(el)

def _handle_beams(self, measure_el, start, end):
def _handle_beams(self, measure_el: lxml.etree._Element, start: int, end: int):
for beam in self.part.iter_all(spt.Beam, start=start, end=end):
# If the beam has only one note, skip it
if len(beam.notes) < 2:
Expand Down Expand Up @@ -400,7 +402,7 @@ def _handle_beams(self, measure_el, start, end):
if note_el.getparent() != beam_el:
beam_el.append(note_el)

def _handle_clef_changes(self, measure_el, start, end):
def _handle_clef_changes(self, measure_el: lxml.etree._Element, start: int, end: int):
for clef in self.part.iter_all(spt.Clef, start=start, end=end):
# Clef element is parent of the note element
if clef.start.t == 0:
Expand All @@ -421,7 +423,7 @@ def _handle_clef_changes(self, measure_el, start, end):
clef_el.set("shape", str(clef.sign))
clef_el.set("line", str(clef.line))

def _handle_ks_changes(self, measure_el, start, end):
def _handle_ks_changes(self, measure_el: lxml.etree._Element, start: int, end: int):
# For key signature changes, we add a new scoreDef element at the beginning of the measure
# and add the key signature element as attributes of the scoreDef element
for key_sig in self.part.iter_all(spt.KeySignature, start=start, end=end):
Expand Down Expand Up @@ -451,7 +453,7 @@ def _handle_ks_changes(self, measure_el, start, end):
parent = measure_el.getparent()
parent.insert(parent.index(measure_el), score_def_el)

def _handle_ts_changes(self, measure_el, start, end):
def _handle_ts_changes(self, measure_el: lxml.etree._Element, start: int, end: int):
# For key signature changes, we add a new scoreDef element at the beginning of the measure
# and add the key signature element as attributes of the scoreDef element
for time_sig in self.part.iter_all(spt.TimeSignature, start=start, end=end):
Expand All @@ -467,7 +469,7 @@ def _handle_ts_changes(self, measure_el, start, end):
score_def_el.set("count", str(time_sig.beats))
score_def_el.set("unit", str(time_sig.beat_type))

def _handle_harmony(self, measure_el, start, end):
def _handle_harmony(self, measure_el: lxml.etree._Element, start: int, end: int):
"""
For harmonies we add a new harm element at the beginning of the measure.
The position doesn't really matter since the tstamp attribute will place it correctly
Expand Down Expand Up @@ -508,7 +510,7 @@ def _handle_harmony(self, measure_el, start, end):
# text is a child element of harmony but not a xml element
harm_el.text = "|" + harmony.text

def _handle_fermata(self, measure_el, start, end):
def _handle_fermata(self, measure_el: lxml.etree._Element, start: int, end: int):
for fermata in self.part.iter_all(spt.Fermata, start=start, end=end):
if fermata.ref is not None:
note = fermata.ref
Expand All @@ -527,7 +529,7 @@ def _handle_fermata(self, measure_el, start, end):
# Set the fermata to be above the staff (the highest staff)
fermata_el.set("staff", "1")

def _handle_barline(self, measure_el, start, end):
def _handle_barline(self, measure_el: lxml.etree._Element, start: int, end: int):
for end_barline in self.part.iter_all(
spt.Ending, start=end, end=end + 1, mode="ending"
):
Expand All @@ -546,6 +548,23 @@ def _handle_barline(self, measure_el, start, end):
):
measure_el.set("left", "rptstart")

def _handle_fingering(self, measure_el: lxml.etree._Element, start: int, end: int):
"""
For fingering we add a new fing element at the end of the measure.
The position doesn't really matter since the startid attribute will place it correctly
"""
for note in self.part.iter_all(spt.Note, start=start, end=end):
if note.technical is not None:
for technical_notation in note.technical:
if isinstance(technical_notation, score.Fingering) and note.id is not None:
fing_el = etree.SubElement(measure_el, "fing")
fing_el.set(XMLNS_ID, "fing-" + self.elc_id())
fing_el.set("startid", note.id)
# Naive way to place the fingering notation
fing_el.set("place", ("above" if note.staff == 1 else "below"))
# text is a child element of fingering but not a xml element
fing_el.text = technical_notation.fingering


@deprecated_alias(parts="score_data")
def save_mei(
Expand Down
13 changes: 13 additions & 0 deletions partitura/io/exportmusicxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,19 @@ def make_note_el(note, dur, voice, counter, n_of_staves):
articulations_e.extend(articulations)
notations.append(articulations_e)

if note.technical:
technical = []
for technical_notation in note.technical:
if isinstance(technical_notation, score.Fingering):
tech_el = etree.Element("fingering")
tech_el.text = str(technical_notation.fingering)
technical.append(tech_el)

if technical:
technical_e = etree.Element("technical")
technical_e.extend(technical)
notations.append(technical_e)

sym_dur = note.symbolic_duration or {}

if sym_dur.get("type") is not None:
Expand Down
14 changes: 14 additions & 0 deletions partitura/io/importmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
Version,
number_pattern,
vnumber_pattern,
fingering_pattern,
MatchTimeSignature,
format_pnote_id,
)
Expand Down Expand Up @@ -594,6 +595,7 @@ def part_from_matchfile(
alter=note.Modifier,
id=note.Anchor,
articulations=articulations,
technical=[],
)

staff_nr = next(
Expand Down Expand Up @@ -622,6 +624,18 @@ def part_from_matchfile(
None,
)

if any(a.startswith("fingering") for a in note.ScoreAttributesList):
note_attributes["technical"].append(
next(
(
score.Fingering(int(a[9:]))
for a in note.ScoreAttributesList
if fingering_pattern.match(a)
),
None,
)
)

# get rid of this if as soon as we have a way to iterate over the
# duration components. For now we have to treat the cases simple
# and compound durations separately.
Expand Down
36 changes: 35 additions & 1 deletion partitura/io/importmusicxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import warnings
import zipfile

from typing import Union, Optional
from typing import Union, Optional, List
import numpy as np
from lxml import etree

Expand Down Expand Up @@ -1244,6 +1244,12 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non
else:
ornaments = {}

technical_e = e.find("notations/technical")
if technical_e is not None:
technical_notations = get_technical_notations(technical_e)
else:
technical_notations = {}

pitch = e.find("pitch")
unpitch = e.find("unpitched")
if pitch is not None:
Expand Down Expand Up @@ -1271,6 +1277,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non
symbolic_duration=symbolic_duration,
articulations=articulations,
ornaments=ornaments,
technical=technical_notations,
steal_proportion=steal_proportion,
doc_order=doc_order,
stem_direction=stem_dir,
Expand Down Expand Up @@ -1309,6 +1316,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non
symbolic_duration=symbolic_duration,
articulations=articulations,
ornaments=ornaments,
technical=technical_notations,
doc_order=doc_order,
stem_direction=stem_dir,
)
Expand Down Expand Up @@ -1338,6 +1346,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non
notehead=notehead,
noteheadstyle=noteheadstylebool,
articulations=articulations,
technical=technical_notations,
symbolic_duration=symbolic_duration,
doc_order=doc_order,
stem_direction=stem_dir,
Expand All @@ -1351,6 +1360,7 @@ def _handle_note(e, position, part, ongoing, prev_note, doc_order, prev_beam=Non
staff=staff,
symbolic_duration=symbolic_duration,
articulations=articulations,
technical=technical_notations,
doc_order=doc_order,
)

Expand Down Expand Up @@ -1648,6 +1658,30 @@ def get_ornaments(e):
return [a for a in ornaments if e.find(a) is not None]


def get_technical_notations(e: etree._Element) -> List[score.NoteTechnicalNotation]:
# For a full list of technical notations
# https://usermanuals.musicxml.com/MusicXML/Content/EL-MusicXML-technical.htm
# for now we only support fingering
technical_notation_parsers = {
"fingering": parse_fingering,
}

technical_notations = [
parser(e.find(a))
for a, parser in technical_notation_parsers.items()
if e.find(a) is not None
]

return technical_notations


def parse_fingering(e: etree._Element) -> score.Fingering:

fingering = score.Fingering(fingering=int(e.text))

return fingering


@deprecated_alias(fn="filename")
def musicxml_to_notearray(
filename,
Expand Down
1 change: 1 addition & 0 deletions partitura/io/matchfile_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

number_pattern = re.compile(r"\d+")
vnumber_pattern = re.compile(r"v\d+")
fingering_pattern = re.compile(r"fingering\d+")

# For matchfiles before 1.0.0.
old_version_pattern = re.compile(r"^(?P<minor>[0-9]+)\.(?P<patch>[0-9]+)")
Expand Down
24 changes: 17 additions & 7 deletions partitura/musicanalysis/performance_codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,8 +608,8 @@ def tempo_by_derivative(

@deprecated_alias(part="score", ppart="performance")
def to_matched_score(
score: ScoreLike,
performance: PerformanceLike,
score: Union[ScoreLike, np.ndarray],
performance: Union[PerformanceLike, np.ndarray],
alignment: list,
include_score_markings=False,
):
Expand All @@ -635,16 +635,26 @@ def to_matched_score(
a["score_id"] = str(a["score_id"])

feature_functions = None
if include_score_markings:
if include_score_markings and not isinstance(score, np.ndarray):
feature_functions = [
"loudness_direction_feature",
"articulation_feature",
"tempo_direction_feature",
"slur_feature",
]

na = note_features.compute_note_array(score, feature_functions=feature_functions)
p_na = performance.note_array()
if isinstance(score, np.ndarray):
na = score
else:
na = note_features.compute_note_array(
score,
feature_functions=feature_functions,
)

if isinstance(performance, np.ndarray):
p_na = performance
else:
p_na = performance.note_array()
part_by_id = dict((n["id"], na[na["id"] == n["id"]]) for n in na)
ppart_by_id = dict((n["id"], p_na[p_na["id"] == n["id"]]) for n in p_na)

Expand Down Expand Up @@ -682,7 +692,7 @@ def to_matched_score(
("p_duration", "f4"),
("velocity", "i4"),
]
if include_score_markings:
if include_score_markings and not isinstance(score, np.ndarray):
fields += [("voice", "i4")]
fields += [
(field, sn.dtype.fields[field][0])
Expand Down Expand Up @@ -763,7 +773,7 @@ def get_time_maps_from_alignment(
# representing the "performeance time" of the position of the score
# onsets
eq_perf_onsets = np.array(
[np.mean(perf_onsets[u]) for u in score_unique_onset_idxs]
[np.mean(perf_onsets[u.astype(int)]) for u in score_unique_onset_idxs]
)

# Get maps
Expand Down
Loading
Loading