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

Estimate symbolic duration for Composite durations #352

Merged
merged 13 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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
39 changes: 34 additions & 5 deletions partitura/io/exportmei.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
fifths_mode_to_key_name,
)
import numpy as np
import warnings
from partitura.utils.misc import deprecated_alias, PathLike
from partitura.utils.music import MEI_DURS_TO_SYMBOLIC, estimate_symbolic_duration

Expand Down Expand Up @@ -295,6 +296,22 @@ def _handle_tuplets(self, measure_el, start, end):
for tuplet in self.part.iter_all(spt.Tuplet, start=start, end=end):
start_note = tuplet.start_note
end_note = tuplet.end_note
if start_note.start.t < start or end_note.end.t > end:
warnings.warn(
"Tuplet start or end note is outside of the measure. Skipping tuplet element."
)
continue
if start_note.start.t > end_note.start.t:
warnings.warn(
"Tuplet start note is after end note. Skipping tuplet element."
)
continue
# Skip if start and end notes are in different voices or staves
if start_note.voice != end_note.voice or start_note.staff != end_note.staff:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why should we skip if we are changing staff? This could be a staff crossing voice once we fix the modeling problem of partitura with these things.
I would allow it, unless it creates problems with the actual version

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true. However, it created some problems with the export. This is quite tricky. The issue comes from the Tuplet objects which should have tests implemented on them. Well, I can either comment out the staff and add a NOTE that this might create errors or add it as a flag on the function (i.e. cross_staff_in_tuplets=True).

warnings.warn(
"Tuplet start and end notes are in different voices or staves. Skipping tuplet element."
)
continue
# Find the note element corresponding to the start note i.e. has the same id value
start_note_el = measure_el.xpath(f".//*[@xml:id='{start_note.id}']")[0]
# Find the note element corresponding to the end note i.e. has the same id value
Expand Down Expand Up @@ -322,6 +339,9 @@ def _handle_tuplets(self, measure_el, start, end):
# Find them from the xml tree
start_note_index = start_note_el.getparent().index(start_note_el)
end_note_index = end_note_el.getparent().index(end_note_el)
# If the start and end note elements are not in order skip (it a weird bug that happens sometimes)
if start_note_index > end_note_index:
continue
xml_el_within_tuplet = [
start_note_el.getparent()[i]
for i in range(start_note_index, end_note_index + 1)
Expand All @@ -347,8 +367,13 @@ def _handle_beams(self, measure_el, start, end):
# If the parent is a chord, the beam element should be added as parent of the chord element
if layer_el.tag == "chord":
parent_el = layer_el.getparent()
insert_index = parent_el.index(layer_el)
layer_el = parent_el
if parent_el.tag == "tuplet":
parent_el = parent_el.getparent()
insert_index = parent_el.index(layer_el.getparent())
layer_el = parent_el
else:
insert_index = parent_el.index(layer_el)
layer_el = parent_el

# Create the beam element
beam_el = etree.Element("beam")
Expand All @@ -363,7 +388,10 @@ def _handle_beams(self, measure_el, start, end):
if note_el.getparent().tag == "tuplet":
beam_el.append(note_el.getparent())
elif note_el.getparent().tag == "chord":
beam_el.append(note_el.getparent())
if note_el.getparent().getparent().tag == "tuplet":
beam_el.append(note_el.getparent().getparent())
else:
beam_el.append(note_el.getparent())
else:
# verify that the note element is not already a child of the beam element
if note_el.getparent() != beam_el:
Expand Down Expand Up @@ -481,8 +509,9 @@ def _handle_fermata(self, measure_el, start, end):
for fermata in self.part.iter_all(spt.Fermata, start=start, end=end):
if fermata.ref is not None:
note = fermata.ref
note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']")[0]
note_el.set("fermata", "above")
note_el = measure_el.xpath(f".//*[@xml:id='{note.id}']")
if len(note_el) > 0:
note_el[0].set("fermata", "above")
else:
fermata_el = etree.SubElement(measure_el, "fermata")
fermata_el.set(XMLNS_ID, "fermata-" + self.elc_id())
Expand Down
3 changes: 3 additions & 0 deletions partitura/io/importmusicxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -1447,6 +1447,9 @@ def handle_tuplets(notations, ongoing, note):

stopping_tuplets.append(tuplet)

# assert that starting tuplet times are before stopping tuplet times
for start_tuplet, stop_tuplet in zip(starting_tuplets, stopping_tuplets):
assert start_tuplet.start_note.start.t < stop_tuplet.end_note.start.t, "Tuplet start time is after tuplet stop time"
return starting_tuplets, stopping_tuplets


Expand Down
105 changes: 75 additions & 30 deletions partitura/score.py
Original file line number Diff line number Diff line change
Expand Up @@ -3846,6 +3846,7 @@ def find_tuplets(part):
start_note = note_tuplet[0]
stop_note = note_tuplet[-1]
tuplet = Tuplet(start_note, stop_note)
assert start_note.start.t <= stop_note.start.t, "The start note of a Tuplet should be before the stop note"
part.add(tuplet, start_note.start.t, stop_note.end.t)
tup_start += actual_notes

Expand Down Expand Up @@ -5142,13 +5143,24 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None:
if len(unique_staff) < part.number_of_staves:
for staff in range(1, part.number_of_staves + 1):
if staff not in unique_staff:
# solution when estimation returns composite durations.
sym_dur = estimate_symbolic_duration(
end_time - start_time, part._quarter_durations[0]
end_time - start_time, part._quarter_durations[0], return_com_durations=True
)
rest = Rest(
symbolic_duration=sym_dur, staff=staff, voice=un_voice.max() + 1
)
part.add(rest, start_time, end_time)
if isinstance(sym_dur, tuple):
st = start_time
for i, sd in enumerate(sym_dur):
et = start_time + symbolic_to_numeric_duration(sd, part._quarter_durations[0])
rest = Rest(
symbolic_duration=sd, staff=staff, voice=un_voice.max() + 1
)
part.add(rest, st, et)
st = et
else:
rest = Rest(
symbolic_duration=sym_dur, staff=staff, voice=un_voice.max() + 1
)
part.add(rest, start_time, end_time)
# Now we fill the rests for each voice
for i in range(len(un_voice)):
note_mask = inverse_map == i
Expand All @@ -5161,27 +5173,49 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None:
min_start_note = notes_per_vocstaff[sort_note_start[0]]
if min_start_note.start.t > start_time:
sym_dur = estimate_symbolic_duration(
min_start_note.start.t - start_time, part._quarter_durations[0]
min_start_note.start.t - start_time, part._quarter_durations[0], return_com_durations=True
)
rest = Rest(
symbolic_duration=sym_dur,
staff=min_start_note.staff,
voice=min_start_note.voice,
)
part.add(rest, start_time, min_start_note.start.t)
# solution when estimation returns composite durations.
if isinstance(sym_dur, tuple):
st = start_time
for i, sd in enumerate(sym_dur):
et = st + symbolic_to_numeric_duration(sd, part._quarter_durations[0])
rest = Rest(
symbolic_duration=sd, staff=min_start_note.staff, voice=min_start_note.voice
)
part.add(rest, st, et)
st = et
else:
rest = Rest(
symbolic_duration=sym_dur,
staff=min_start_note.staff,
voice=min_start_note.voice,
)
part.add(rest, start_time, min_start_note.start.t)

# get note with max end.t and fill the rest after it if needed
min_end_note = notes_per_vocstaff[sort_note_end[-1]]
if min_end_note.end.t < end_time:
sym_dur = estimate_symbolic_duration(
end_time - min_end_note.end.t, part._quarter_durations[0]
end_time - min_end_note.end.t, part._quarter_durations[0], return_com_durations=True
)
rest = Rest(
symbolic_duration=sym_dur,
staff=min_end_note.staff,
voice=min_end_note.voice,
)
part.add(rest, min_end_note.end.t, end_time)
# solution when estimation returns composite durations.
if isinstance(sym_dur, tuple):
st = min_end_note.end.t
for i, sd in enumerate(sym_dur):
et = st + symbolic_to_numeric_duration(sd, part._quarter_durations[0])
rest = Rest(
symbolic_duration=sd, staff=min_end_note.staff, voice=min_end_note.voice
)
part.add(rest, st, et)
st = et
else:
rest = Rest(
symbolic_duration=sym_dur,
staff=min_end_note.staff,
voice=min_end_note.voice,
)
part.add(rest, min_end_note.end.t, end_time)

if len(sort_note_start) <= 1:
continue
Expand All @@ -5194,18 +5228,29 @@ def _fill_rests_within_measure(measure: Measure, part: Part) -> None:
sym_dur = estimate_symbolic_duration(
notes_per_vocstaff[sort_note_start[i]].start.t
- notes_per_vocstaff[sort_note_end[i - 1]].end.t,
part._quarter_durations[0],
)
rest = Rest(
symbolic_duration=sym_dur,
staff=notes_per_vocstaff[sort_note_end[i - 1]].staff,
voice=notes_per_vocstaff[sort_note_end[i - 1]].voice,
)
part.add(
rest,
notes_per_vocstaff[sort_note_end[i - 1]].end.t,
notes_per_vocstaff[sort_note_start[i]].start.t,
part._quarter_durations[0], return_com_durations=True
)
if isinstance(sym_dur, tuple):
st = notes_per_vocstaff[sort_note_end[i - 1]].end.t
for i, sd in enumerate(sym_dur):
et = st + symbolic_to_numeric_duration(sd, part._quarter_durations[0])
rest = Rest(
symbolic_duration=sd, staff=notes_per_vocstaff[sort_note_end[i - 1]].staff,
voice=notes_per_vocstaff[sort_note_end[i - 1]].voice
)
part.add(rest, st, et)
st = et
else:
rest = Rest(
symbolic_duration=sym_dur,
staff=notes_per_vocstaff[sort_note_end[i - 1]].staff,
voice=notes_per_vocstaff[sort_note_end[i - 1]].voice,
)
part.add(
rest,
notes_per_vocstaff[sort_note_end[i - 1]].end.t,
notes_per_vocstaff[sort_note_start[i]].start.t,
)


def _fill_rests_global(
Expand Down
51 changes: 39 additions & 12 deletions partitura/utils/music.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
from __future__ import annotations
import copy
from collections import defaultdict
import re
import re, math
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, List
from typing import Union, Callable, Optional, TYPE_CHECKING, Tuple, Dict, Any, List
from partitura.utils.generic import find_nearest, search, iter_current_next
import partitura
from tempfile import TemporaryDirectory
Expand Down Expand Up @@ -171,6 +171,8 @@ class MIDITokenizer(object):
]
)



SYM_DURS = [
{"type": "256th", "dots": 0},
{"type": "256th", "dots": 1},
Expand Down Expand Up @@ -319,6 +321,18 @@ class MIDITokenizer(object):
# Standard tuning frequency of A4 in Hz
A4 = 440.0

COMPOSITE_DURS = np.array(
fosfrancesco marked this conversation as resolved.
Show resolved Hide resolved
[1 + 4/16, 1 + 4/32, 2+4/8, 2+4/16, 2+4/32]
)

SYM_COMPOSITE_DURS = [
({"type": "quarter", "dots": 0}, {"type": "16nd", "dots": 0}),
({"type": "quarter", "dots": 0}, {"type": "32nd", "dots": 0}),
({"type": "half", "dots": 0}, {"type": "eighth", "dots": 0}),
({"type": "half", "dots": 0}, {"type": "16th", "dots": 0}),
({"type": "half", "dots": 0}, {"type": "32nd", "dots": 0})
]


def ensure_notearray(notearray_or_part, *args, **kwargs):
"""
Expand Down Expand Up @@ -894,7 +908,7 @@ def key_int_to_mode(mode):
raise ValueError("Unknown mode {}".format(mode))


def estimate_symbolic_duration(dur, div, eps=10**-3):
def estimate_symbolic_duration(dur, div, eps=10**-3, return_com_durations=False) -> Union[Union[Dict[str, Any], Tuple[Dict[str, Any]]], None]:
"""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
Expand All @@ -915,10 +929,15 @@ def estimate_symbolic_duration(dur, div, eps=10**-3):
Number of units per quarter note
eps : float, optional (default: 10**-3)
Tolerance in case of imprecise matches
return_com_durations : bool, optional (default: False)
If True, return composite durations as well.

Returns
-------

out: Union[Union[Dict[str, Any], Tuple[Dict[str, Any]]], None]
Symbolic duration as a dictionary, or None if no matching
duration is found. When a composite duration is found, then it returns a tuple of symbolic durations.
The returned tuple should be tied notes.
fosfrancesco marked this conversation as resolved.
Show resolved Hide resolved

Examples
--------
Expand All @@ -941,14 +960,22 @@ def estimate_symbolic_duration(dur, div, eps=10**-3):
if np.abs(qdur - DURS[i]) < eps:
return SYM_DURS[i].copy()
else:
# NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes.
type = SYM_DURS[i + 3]["type"]
normal_notes = 2
return {
"type": type,
"actual_notes": math.ceil(normal_notes / qdur),
"normal_notes": normal_notes,
}
# Note when the duration is not found, the we are left with two solutions:
# 1. The duration is a tuplet
# 2. The duration is a composite duration
# For composite duration. We can use the following approach:
j = find_nearest(COMPOSITE_DURS, qdur)
if np.abs(qdur - COMPOSITE_DURS[j]) < eps and return_com_durations:
return copy.copy(SYM_COMPOSITE_DURS[j])
else:
# NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes.
type = SYM_DURS[i + 3]["type"]
normal_notes = 2
return {
"type": type,
"actual_notes": math.ceil(normal_notes / qdur),
"normal_notes": normal_notes,
}
manoskary marked this conversation as resolved.
Show resolved Hide resolved


def to_quarter_tempo(unit, tempo):
Expand Down
Loading