Skip to content

Commit

Permalink
Merge pull request #403 from CPJKU/fingering
Browse files Browse the repository at this point in the history
 Support for Fingering Annotations and Markings.
  • Loading branch information
fosfrancesco authored Dec 19, 2024
2 parents 9b82412 + 86117e8 commit bb1adac
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 17 deletions.
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

0 comments on commit bb1adac

Please sign in to comment.