Skip to content

Commit

Permalink
Merge pull request #372 from CPJKU/estimate_symbolic_duration_fix
Browse files Browse the repository at this point in the history
New solution for estimate_symbolic_duration more robust covers more cases without resulting to weird or incorrect values.
  • Loading branch information
manoskary authored Oct 2, 2024
2 parents 3520066 + c159bc9 commit ca3ce79
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 6 deletions.
89 changes: 88 additions & 1 deletion partitura/utils/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,25 @@
{"type": "long", "dots": 3},
]

# Straight durs do not include copies for naming or dots, when searching they work better for base triplet types in `estimate_symbolic_duration`.
STRAIGHT_DURS = np.array(
[ 4 / 256, 4 / 128, 4 / 64, 4 / 32, 4 / 16, 4 / 8, 4 / 4, 4 / 2, 4 / 1, 4 / 0.5, 4 / 0.25]
)

SYM_STRAIGHT_DURS = [
{"type": "256th", "dots": 0},
{"type": "128th", "dots": 0},
{"type": "64th", "dots": 0},
{"type": "32nd", "dots": 0},
{"type": "16th", "dots": 0},
{"type": "eighth", "dots": 0},
{"type": "quarter", "dots": 0},
{"type": "half", "dots": 0},
{"type": "whole", "dots": 0},
{"type": "breve", "dots": 0},
{"type": "long", "dots": 0},
]

MAJOR_KEYS = [
"Cb",
"Gb",
Expand Down Expand Up @@ -281,14 +300,82 @@
# Standard tuning frequency of A4 in Hz
A4 = 440.0

COMPOSITE_DURS = np.array([1 + 4 / 32, 1 + 4 / 16, 2 + 4 / 32, 2 + 4 / 16, 2 + 4 / 8])
COMPOSITE_DURS = np.array(
[
1/4 + 1/6,
1/2 + 1/12,
1/2 + 1/3,
1/2 + 1/4 + 1/6,
1 + 1/12,
1 + 1 / 8,
1 + 1 / 6,
1 + 1 / 4,
1 + 1 / 4 + 1 / 6,
1 + 1 / 2 + 1 / 12,
1 + 1 / 2 + 1 / 6,
1 + 1 / 2 + 1 / 3,
1 + 1 / 2 + 1 / 4 + 1 / 6,
2 + 1 / 12,
2 + 1 / 8,
2 + 1 / 6,
2 + 1 / 4,
2 + 1 / 3,
2 + 1 / 4 + 1 / 6,
2 + 1 / 2,
2 + 1 / 2 + 1 / 12,
2 + 2 / 3,
2 + 1 / 2 + 1 / 4,
2 + 1 / 2 + 1 / 3,
2 + 1 / 2 + 1 / 4 + 1 / 6,
3 + 1 / 12,
3 + 1 / 8,
3 + 1 / 6,
3 + 1 / 4,
3 + 1 / 3,
3 + 1 / 4 + 1 / 6,
3 + 1 / 2 + 1 / 12,
3 + 2 / 3,
3 + 1 / 2 + 1 / 3,
3 + 1 / 2 + 1 / 4 + 1 / 6,
]
)

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


Expand Down
21 changes: 17 additions & 4 deletions partitura/utils/music.py
Original file line number Diff line number Diff line change
Expand Up @@ -762,15 +762,28 @@ def estimate_symbolic_duration(
# 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])
if np.abs(qdur - COMPOSITE_DURS[j]) < eps:
if return_com_durations:
return copy.copy(SYM_COMPOSITE_DURS[j])
else:
warnings.warn(f"Quarter duration {qdur} from {dur}/{div} is a composite"
f"duration but composite durations are not allowed. Returning empty symbolic duration.")
return {}
# Naive condition to only apply tuplet estimation if the quarter duration is less than a bar (4)
elif qdur > 4:
warnings.warn(f"Quarter duration {qdur} from {dur}/{div} is not a tuplet or composite duration."
f"Returning empty symbolic duration.")
return {}
else:
i = np.searchsorted(STRAIGHT_DURS, qdur, side="left") - 1
# NOTE: Guess tuplets (Naive) it doesn't cover composite durations from tied notes.
type = SYM_DURS[i + 3]["type"]
type = SYM_STRAIGHT_DURS[i+1]["type"]
normal_notes = 2
while (normal_notes * STRAIGHT_DURS[i + 1] / qdur) % 1 > eps:
normal_notes += 1
return {
"type": type,
"actual_notes": math.ceil(normal_notes / qdur),
"actual_notes": math.ceil(normal_notes * STRAIGHT_DURS[i+1] / qdur),
"normal_notes": normal_notes,
}

Expand Down
2 changes: 1 addition & 1 deletion tests/test_midi_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def make_triplets_example_2():
fill_track(track, notes, divs)
# target:
actual_notes = [5] * 5 + [3] * 3
normal_notes = [2] * 5 + [2] * 3
normal_notes = [4] * 5 + [2] * 3
return mid, actual_notes, normal_notes


Expand Down
66 changes: 66 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,3 +643,69 @@ def test_tokenize2(self):
pt_tokens = [tok for tok in pt_tokens if not tok.startswith("Velocity")]
mtok_tokens = [tok for tok in mtok_tokens if not tok.startswith("Velocity")]
self.assertTrue(pt_tokens == mtok_tokens)


class TestSymbolicDurationEstimator(unittest.TestCase):
def test_estimate_symbolic_duration(self):
"""
Test `estimate_symbolic_duration`
"""
divs = 12
quarters = 4
expected = [
{},
{'type': '32nd', 'actual_notes': 3, 'normal_notes': 2},
{'type': '16th', 'actual_notes': 3, 'normal_notes': 2},
{'type': '16th', 'dots': 0},
{'type': 'eighth', 'actual_notes': 3, 'normal_notes': 2},
({'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
{'type': 'eighth', 'dots': 0},
({'type': 'eighth', 'dots': 0}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
{'type': 'quarter', 'actual_notes': 3, 'normal_notes': 2},
{'type': 'eighth', 'dots': 1},
({'type': 'eighth', 'dots': 0}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'eighth', 'dots': 1}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
{'type': 'quarter', 'dots': 0},
({'type': 'quarter', 'dots': 0}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'quarter', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'quarter', 'dots': 0}, {'type': '16th', 'dots': 0}),
{'type': 'half', 'actual_notes': 3, 'normal_notes': 2},
({'type': 'quarter', 'dots': 0}, {'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
{'type': 'quarter', 'dots': 1},
({'type': 'quarter', 'dots': 1}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'quarter', 'dots': 1}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
{'type': 'quarter', 'dots': 2},
({'type': 'quarter', 'dots': 1}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'quarter', 'dots': 2}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
{'type': 'half', 'dots': 0},
({'type': 'half', 'dots': 0}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'half', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'half', 'dots': 0}, {'type': '16th', 'dots': 0}),
({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'half', 'dots': 0}, {'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0}),
({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'half', 'dots': 0}, {'type': 'quarter', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 1}),
({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 0}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'half', 'dots': 0}, {'type': 'eighth', 'dots': 1}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
{'type': 'half', 'dots': 1},
({'type': 'half', 'dots': 1}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'half', 'dots': 1}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'half', 'dots': 1}, {'type': '16th', 'dots': 0}),
({'type': 'half', 'dots': 1}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'half', 'dots': 1}, {'type': '16th', 'dots': 0}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
{'type': 'half', 'dots': 2},
({'type': 'half', 'dots': 2}, {'type': '32nd', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'half', 'dots': 1}, {'type': 'quarter', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
{'type': 'half', 'dots': 3},
({'type': 'half', 'dots': 2}, {'type': 'eighth', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
({'type': 'half', 'dots': 3}, {'type': '16th', 'dots': 0, 'actual_notes': 3, 'normal_notes': 2}),
]
predicted = []
for k in range(divs * 0, divs * quarters):
a = partitura.utils.estimate_symbolic_duration(
k, divs, return_com_durations=True)
predicted.append(a)
self.assertTrue(all([expected[i] == predicted[i] for i in range(len(expected))]))

0 comments on commit ca3ce79

Please sign in to comment.