|
|
|
@@ -89,6 +89,28 @@ class RhythmLibraryTests(unittest.TestCase):
|
|
|
|
|
self.assertEqual([bar.attacks() for bar in snippet.bars], [(0,), ()])
|
|
|
|
|
self.assertEqual([bar.events[0].continuation for bar in snippet.bars], [False, True])
|
|
|
|
|
self.assertEqual([bar.length for bar in snippet.bars], [4, 4])
|
|
|
|
|
def test_third_series_supports_long_notes_and_rests_in_every_meter(self):
|
|
|
|
|
for beats, duration, kind, dots in ((2, 2, 'half', 0), (3, 3, 'half', 1), (4, 4, 'whole', 0), (5, 4, 'whole', 0)):
|
|
|
|
|
for sounding in (False, True):
|
|
|
|
|
with self.subTest(beats=beats, sounding=sounding):
|
|
|
|
|
content = tests.test_musicxml.xml_note(duration=duration, kind=kind, extra='<dot/>' if dots else '') if sounding else f'<note><rest/><duration>{duration}</duration><type>{kind}</type>{"<dot/>" if dots else ""}</note>'
|
|
|
|
|
if beats == 5:
|
|
|
|
|
content += tests.test_musicxml.xml_note(duration=1, kind='quarter')
|
|
|
|
|
snippet = self.load({f'{beats}-4/3/0001.xml': xml_bars([content], beats=beats)}).for_meter(f'{beats}/4')[0]
|
|
|
|
|
self.assertEqual(snippet.bars[0].events[0], generator.rhythm.Event(fractions.Fraction(), fractions.Fraction(duration), sounding, kind, dots))
|
|
|
|
|
self.assertEqual(snippet.bars[0].length, beats)
|
|
|
|
|
def test_all_three_series_participate_in_deterministic_long_note_selection(self):
|
|
|
|
|
silence = '<note><rest/><duration>4</duration><type>whole</type></note>'
|
|
|
|
|
halves = tests.test_musicxml.xml_note(duration=2, kind='half') * 2
|
|
|
|
|
library = self.load({'4-4/1/0001.xml': xml_bars([silence] * 2), '4-4/2/0001.xml': xml_bars([silence]), '4-4/3/0001.xml': xml_bars([halves])})
|
|
|
|
|
self.assertEqual(len(library.for_meter('4/4')), 3)
|
|
|
|
|
measures = melody_measures(8)
|
|
|
|
|
plan = generator.accompaniment.plan_section(measures, library)
|
|
|
|
|
expected = library.for_meter('4/4')[-1].bars[0]
|
|
|
|
|
for lane in (plan.acoustic_bass, plan.acoustic_upper, plan.electric):
|
|
|
|
|
self.assertEqual(lane, (expected,) * 8)
|
|
|
|
|
reordered = generator.rhythm.Library(tuple(reversed(library.for_meter('4/4'))))
|
|
|
|
|
self.assertEqual(generator.accompaniment.plan_section(measures, reordered), plan)
|
|
|
|
|
def test_beat_comparison_counts_held_half_notes_and_quarter_reattacks(self):
|
|
|
|
|
half = held_half_bar()
|
|
|
|
|
silent_half = held_half_bar(False)
|
|
|
|
@@ -123,7 +145,7 @@ class RhythmLibraryTests(unittest.TestCase):
|
|
|
|
|
with self.assertRaisesRegex(ValueError, 'empty'):
|
|
|
|
|
self.load({})
|
|
|
|
|
source = xml_bars([tests.test_musicxml.xml_note()])
|
|
|
|
|
for path in ('snippet.xml', '4-4/snippet.xml', '4-4/3/snippet.xml', '4-4/1/nested/snippet.xml', '6-8/1/snippet.xml'):
|
|
|
|
|
for path in ('snippet.xml', '4-4/snippet.xml', '4-4/4/snippet.xml', '4-4/1/nested/snippet.xml', '6-8/1/snippet.xml'):
|
|
|
|
|
with self.subTest(path=path), self.assertRaisesRegex(ValueError, 'meter/series'):
|
|
|
|
|
self.load({path: source})
|
|
|
|
|
with self.assertRaisesRegex(ValueError, 'meter disagrees'):
|
|
|
|
@@ -264,6 +286,40 @@ class AccompanimentPlanningTests(unittest.TestCase):
|
|
|
|
|
self.assertEqual(generator.accompaniment.sounding_attacks(plan.acoustic_upper[2], contexts[2]), ())
|
|
|
|
|
|
|
|
|
|
class AccompanimentRealizationTests(unittest.TestCase):
|
|
|
|
|
def test_electric_preserves_half_dotted_half_and_whole_durations(self):
|
|
|
|
|
for beats, duration, kind, dots in ((2, 2, 'half', 0), (3, 3, 'half', 1), (4, 4, 'whole', 0), (5, 4, 'whole', 0)):
|
|
|
|
|
with self.subTest(beats=beats):
|
|
|
|
|
measure = melody_measures(1, beats=beats)[0]
|
|
|
|
|
events = [generator.rhythm.Event(fractions.Fraction(), fractions.Fraction(duration), True, kind, dots)]
|
|
|
|
|
if beats == 5:
|
|
|
|
|
events.append(generator.rhythm.Event(fractions.Fraction(4), fractions.Fraction(1), False, 'quarter'))
|
|
|
|
|
notes = generator.accompaniment.realize(measure, generator.rhythm.Bar(tuple(events), fractions.Fraction(beats)), None, lane='electric')
|
|
|
|
|
self.assertEqual(notes[0]['written'], {'type': kind, 'dots': dots})
|
|
|
|
|
self.assertEqual(generator.model.fraction(notes[0]['duration']), duration)
|
|
|
|
|
self.assertEqual(sum(generator.model.fraction(n['duration']) for n in notes), beats)
|
|
|
|
|
self.assertEqual([generator.model.fraction(n['onset']) for n in notes if n['kind'] == 'note'], [0])
|
|
|
|
|
def test_clipped_long_notes_use_single_note_spelling_then_rests(self):
|
|
|
|
|
bar = generator.rhythm.Bar((generator.rhythm.Event(fractions.Fraction(), fractions.Fraction(4), True, 'whole'),), fractions.Fraction(4))
|
|
|
|
|
for kind in ('major', 'none'):
|
|
|
|
|
for offset, divisions, expected in ((5, 2, fractions.Fraction(2)), (7, 2, fractions.Fraction(7, 2)), (4, 3, fractions.Fraction(4, 3))):
|
|
|
|
|
with self.subTest(kind=kind, offset=offset, divisions=divisions):
|
|
|
|
|
content = tests.test_parts.xml_harmony() + tests.test_parts.xml_harmony('F', kind, offset=offset) + tests.test_musicxml.xml_note(duration=4 * divisions)
|
|
|
|
|
measure = tests.test_musicxml.parse(xml_bars([content], divisions=divisions))['staves'][0]['measures'][0]
|
|
|
|
|
notes = generator.accompaniment.realize(measure, bar, None, lane='electric')
|
|
|
|
|
self.assertEqual(generator.model.fraction(notes[0]['duration']), expected)
|
|
|
|
|
self.assertEqual([n['kind'] for n in notes], ['note'] + ['rest'] * (len(notes) - 1))
|
|
|
|
|
self.assertEqual(sum(generator.model.fraction(n['duration']) for n in notes), 4)
|
|
|
|
|
if divisions == 2:
|
|
|
|
|
self.assertTrue(all('ratio' not in n['written'] for n in notes))
|
|
|
|
|
for left, right in zip(notes, notes[1:]):
|
|
|
|
|
self.assertEqual(generator.model.fraction(left['onset']) + generator.model.fraction(left['duration']), generator.model.fraction(right['onset']))
|
|
|
|
|
def test_long_note_pickup_is_clipped_without_extending_the_measure(self):
|
|
|
|
|
bar = generator.rhythm.Bar((generator.rhythm.Event(fractions.Fraction(), fractions.Fraction(4), True, 'whole'),), fractions.Fraction(4))
|
|
|
|
|
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(duration=3, kind='half', extra='<dot/>')
|
|
|
|
|
measure = tests.test_musicxml.parse(xml_bars([content], implicit=True))['staves'][0]['measures'][0]
|
|
|
|
|
notes = generator.accompaniment.realize(measure, bar, None, lane='electric')
|
|
|
|
|
self.assertEqual(timing(notes), [(0, 3, 'note')])
|
|
|
|
|
self.assertEqual(notes[0]['written'], {'type': 'half', 'dots': 1})
|
|
|
|
|
def test_harmonic_gating_removes_dangling_beams(self):
|
|
|
|
|
content = tests.test_parts.xml_harmony() + tests.test_parts.xml_harmony(kind='none', offset=1) + tests.test_musicxml.xml_note(duration=8)
|
|
|
|
|
measure = tests.test_musicxml.parse(xml_bars([content], divisions=2))['staves'][0]['measures'][0]
|
|
|
|
@@ -278,7 +334,7 @@ class AccompanimentRealizationTests(unittest.TestCase):
|
|
|
|
|
measure = tests.test_musicxml.parse(xml_bars([content], divisions=2))['staves'][0]['measures'][0]
|
|
|
|
|
before = copy.deepcopy(measure)
|
|
|
|
|
events = generator.accompaniment.realize(measure, bar, None)
|
|
|
|
|
self.assertEqual(timing(events), [(0, fractions.Fraction(3, 2), 'note'), (fractions.Fraction(3, 2), fractions.Fraction(5, 2), 'rest')])
|
|
|
|
|
self.assertEqual(timing(events), [(0, fractions.Fraction(3, 2), 'note'), (fractions.Fraction(3, 2), 2, 'rest'), (fractions.Fraction(7, 2), fractions.Fraction(1, 2), 'rest')])
|
|
|
|
|
self.assertEqual(events[0]['_harmony']['root']['letter'], 'c')
|
|
|
|
|
self.assertEqual(measure, before)
|
|
|
|
|
def test_chord_changes_preserve_authored_rhythm_rests_until_next_attack(self):
|
|
|
|
@@ -293,9 +349,10 @@ class AccompanimentRealizationTests(unittest.TestCase):
|
|
|
|
|
bar = generator.rhythm.Bar((generator.rhythm.Event(fractions.Fraction(), fractions.Fraction(4), True, 'whole'),), fractions.Fraction(4))
|
|
|
|
|
content = tests.test_parts.xml_harmony() + tests.test_parts.xml_harmony('F') + tests.test_parts.xml_harmony('F', offset=2) + tests.test_musicxml.xml_note()
|
|
|
|
|
measure = tests.test_musicxml.parse(xml_bars([content]))['staves'][0]['measures'][0]
|
|
|
|
|
events = generator.accompaniment.realize(measure, bar, None)
|
|
|
|
|
self.assertEqual(timing(events), [(0, 4, 'note')])
|
|
|
|
|
self.assertEqual(events[0]['_harmony']['root']['letter'], 'f')
|
|
|
|
|
for lane in ('bass', 'upper', 'electric'):
|
|
|
|
|
events = generator.accompaniment.realize(measure, bar, None, lane=lane)
|
|
|
|
|
self.assertEqual(timing(events), [(0, 4, 'note')])
|
|
|
|
|
self.assertEqual(events[0]['_harmony']['root']['letter'], 'f')
|
|
|
|
|
def test_pickup_uses_right_edge_of_nominal_bar_with_exact_clipping(self):
|
|
|
|
|
bar = generator.rhythm.Bar((generator.rhythm.Event(fractions.Fraction(), fractions.Fraction(3), False, 'half', 1), generator.rhythm.Event(fractions.Fraction(3), fractions.Fraction(1), True, 'quarter')), fractions.Fraction(4))
|
|
|
|
|
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(duration=3, kind='quarter', extra='<dot/>')
|
|
|
|
|