Generation
ci / checks (push) Successful in 48m7s

This commit is contained in:
khodowany
2026-09-30 20:30:29 -05:00
parent 09656a9d60
commit 7b60182a3d
3 changed files with 68 additions and 11 deletions
+64 -9
View File
@@ -210,6 +210,45 @@ class AccompanimentPlanningTests(unittest.TestCase):
for bar in plan.electric[:3]:
self.assertIn(len(bar.attacks()), {beats // 2, (beats + 1) // 2})
self.assertLessEqual(len(plan.electric[3].attacks()), (beats + 1) // 2 + 1)
def test_upper_density_targets_three_attacks_per_four_quarter_beats(self):
library = tests.rhythm_fixtures.library()
for beats, counts in ((2, {1, 2}), (3, {2}), (4, {3}), (5, {4})):
with self.subTest(beats=beats):
plan = generator.accompaniment.plan_section(melody_measures(8, beats=beats, dense=True), library)
for bar in plan.acoustic_upper[:3]:
self.assertIn(len(bar.attacks()), counts)
for bar in plan.acoustic_bass[:3]:
self.assertIn(len(bar.attacks()), {beats // 2, (beats + 1) // 2})
self.assertEqual(plan.acoustic_upper[:4], plan.acoustic_upper[4:])
def test_upper_density_scales_with_pickup_and_active_harmony_duration(self):
for beats in (1, 2, 3):
for pickup in (False, True):
with self.subTest(beats=beats, pickup=pickup):
length = beats if pickup else 4
content = tests.test_parts.xml_harmony(offset=0 if pickup else 4 - beats) + tests.test_musicxml.xml_note(duration=1, kind='quarter') * length
measure = tests.test_musicxml.parse(xml_bars([content], implicit=pickup))['staves'][0]['measures'][0]
item = generator.accompaniment.context(measure, None)
bar = quarter_bar(4, {0, 1, 2, 3})
self.assertEqual(item.offset, 4 - beats if pickup else 0)
self.assertEqual(len(generator.accompaniment.sounding_attacks(bar, item)), beats)
self.assertEqual(generator.accompaniment.rank(bar, [(item, ())], 'upper')[2], fractions.Fraction(beats, 4))
def test_harmonic_coverage_outweighs_upper_density_target(self):
content = ''.join(tests.test_parts.xml_harmony(root, offset=i) for i, root in enumerate(('C', 'F', 'G', 'C'))) + tests.test_musicxml.xml_note()
measure = tests.test_musicxml.parse(xml_bars([content]))['staves'][0]['measures'][0]
item = generator.accompaniment.context(measure, None)
sparse, covered = quarter_bar(4, {0, 1, 2}), quarter_bar(4, {0, 1, 2, 3})
sparse_rank = generator.accompaniment.rank(sparse, [(item, ())], 'upper')
covered_rank = generator.accompaniment.rank(covered, [(item, ())], 'upper')
self.assertEqual((sparse_rank[2], covered_rank[2]), (0, 1))
self.assertLess(covered_rank, sparse_rank)
library = generator.rhythm.Library(tuple(generator.rhythm.Snippet(name, '4/4', (bar,)) for name, bar in (('sparse', sparse), ('covered', covered))))
plan = generator.accompaniment.plan_section([measure] * 3, library)
self.assertEqual(plan.acoustic_upper, (covered,) * 3)
def test_upper_density_is_not_an_attack_cap(self):
bar = quarter_bar(4, {0, 1, 2, 3})
library = generator.rhythm.Library((generator.rhythm.Snippet('dense', '4/4', (bar,)),))
plan = generator.accompaniment.plan_section(melody_measures(4, dense=True), library)
self.assertEqual(plan.acoustic_upper, (bar,) * 4)
def test_section_repeats_four_bar_motif_with_at_most_one_beat_variation(self):
for beats in (2, 3, 4, 5):
measures = melody_measures(12, beats=beats, dense=True)
@@ -227,17 +266,33 @@ class AccompanimentPlanningTests(unittest.TestCase):
reordered = generator.rhythm.Library(tuple(reversed([s for group in library.snippets.values() for s in group])))
self.assertEqual(generator.accompaniment.plan_section(measures, reordered), plan)
self.assertEqual(measures, before)
def test_acoustic_and_electric_coordinate_when_space_is_available(self):
def test_acoustic_and_electric_coordinate_when_target_density_has_gaps(self):
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note()
measures = tests.test_musicxml.parse(xml_bars([content] * 4))['staves'][0]['measures']
plan = generator.accompaniment.plan_section(measures, tests.rhythm_fixtures.library())
for index in range(3):
acoustic, electric = set(plan.acoustic_upper[index].attacks()), set(plan.electric[index].attacks())
self.assertTrue(acoustic)
self.assertTrue(electric)
self.assertFalse(acoustic & electric)
self.assertNotIn(0, acoustic | electric)
self.assertGreater(len(acoustic), len(electric))
bass = quarter_bar(4, {0, 2})
electric = quarter_bar(4, {1, 3})
upper = generator.rhythm.Bar(tuple(generator.rhythm.Event(fractions.Fraction(i, 2), fractions.Fraction(1, 2), i in (1, 3, 5), 'eighth') for i in range(8)), fractions.Fraction(4))
library = generator.rhythm.Library(tuple(generator.rhythm.Snippet(name, '4/4', (bar,)) for name, bar in (('bass', bass), ('electric', electric), ('upper', upper))))
plan = generator.accompaniment.plan_section(measures, library)
self.assertEqual(plan.acoustic_bass, (bass,) * 4)
self.assertEqual(plan.acoustic_upper, (upper,) * 4)
self.assertEqual(plan.electric, (electric,) * 4)
def test_equal_density_candidates_favor_gaps_around_melody_and_companions(self):
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note()
measure = tests.test_musicxml.parse(xml_bars([content]))['staves'][0]['measures'][0]
item = generator.accompaniment.context(measure, None)
for role, companions, collision, gap in (('upper', (), {0, 1, 2}, {1, 2, 3}), ('electric', (2,), {0, 2}, {1, 3})):
with self.subTest(role=role):
ranks = [generator.accompaniment.rank(quarter_bar(4, attacks), [(item, companions)], role) for attacks in (gap, collision)]
self.assertLess(ranks[0], ranks[1])
def test_equal_density_candidates_consider_sustained_melody(self):
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(duration=2, kind='half') + '<note><rest/><duration>2</duration><type>half</type></note>'
measure = tests.test_musicxml.parse(xml_bars([content]))['staves'][0]['measures'][0]
item = generator.accompaniment.context(measure, None)
occupied, gap = quarter_bar(4, {1, 3}), quarter_bar(4, {2, 3})
for role in ('upper', 'electric'):
with self.subTest(role=role):
self.assertLess(generator.accompaniment.rank(gap, [(item, ())], role), generator.accompaniment.rank(occupied, [(item, ())], role))
def test_dense_melody_allows_collisions_and_keeps_accompaniment_audible(self):
measures = melody_measures(8, dense=True)
plan = generator.accompaniment.plan_section(measures, tests.rhythm_fixtures.library())