Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,118 @@
|
||||
import copy
|
||||
import fractions
|
||||
import pathlib
|
||||
import unittest
|
||||
|
||||
import generator.arrange.form
|
||||
import generator.arrange.generate
|
||||
import generator.arrange.pickup
|
||||
import generator.model
|
||||
import generator.musicxml
|
||||
import generator.parts
|
||||
import generator.playback
|
||||
import generator.score
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_musicxml
|
||||
import tests.test_parts
|
||||
|
||||
def source(pickup, body=None, *, implicit=True, divisions=1):
|
||||
body = body or tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(duration=4 * divisions)
|
||||
xml = tests.test_musicxml.xml_score(pickup, implicit=implicit, divisions=divisions)
|
||||
return tests.test_musicxml.parse(xml.replace('</measure></part>', '</measure><measure number="2">' + body + '</measure></part>'))
|
||||
|
||||
class ArrangementPickupTests(unittest.TestCase):
|
||||
def test_catalog_body_lengths_and_part_timelines(self):
|
||||
for path in sorted(pathlib.Path('data/songs').glob('*.xml')):
|
||||
document = generator.musicxml.load_musicxml(path)
|
||||
before = copy.deepcopy(document)
|
||||
prepared = generator.parts.prepare_document(document)
|
||||
plans = generator.arrange.generate.plan_arrangement(prepared, tests.rhythm_fixtures.library())
|
||||
timelines = []
|
||||
for part in generator.parts.PART_TYPES:
|
||||
with self.subTest(song=path.stem, part=part):
|
||||
result = generator.arrange.generate._arrange_document(prepared, part, plans)
|
||||
generator.model.validate_document(result)
|
||||
measures = result['staves'][0]['measures']
|
||||
self.assertEqual(len(measures), 176)
|
||||
self.assertEqual(sum(generator.model.fraction(m['duration']) for m in measures), 704)
|
||||
timelines.append([m['duration'] for m in measures])
|
||||
if path.stem == 'autumn-leaves' and part in ('vocal', 'electric'):
|
||||
for boundary in ([8, 136] if part == 'vocal' else [40]):
|
||||
notes = [n for n in generator.score.measure_notes(measures[boundary - 1]) if generator.model.fraction(n['onset']) >= 1 and n['pitches']]
|
||||
self.assertEqual([generator.model.fraction(n['onset']) for n in notes], [1, 2, 3])
|
||||
self.assertEqual([n['pitches'][-1]['letter'] for n in notes], ['d', 'e', 'f'])
|
||||
self.assertTrue(all(n['pitches'][-1]['octave'] == (5 if part == 'electric' else 4) for n in notes))
|
||||
self.assertFalse(measures[boundary - 1]['attributes'])
|
||||
self.assertTrue(all(timeline == timelines[0] for timeline in timelines))
|
||||
self.assertEqual(document, before)
|
||||
def test_unmarked_leading_rests_are_not_pickups(self):
|
||||
opening = '<note><rest/><duration>1</duration><type>quarter</type></note>' + tests.test_musicxml.xml_note(duration=3, kind='half', extra='<dot/>')
|
||||
document = source(opening, implicit=False)
|
||||
result = generator.arrange.generate.arrange_document(document, 'vocal', rhythms=tests.rhythm_fixtures.library())
|
||||
self.assertEqual(len(result['staves'][0]['measures']), 14)
|
||||
self.assertEqual(generator.score.body_start_measure_index(document['staves'][0]['measures']), 0)
|
||||
def test_short_pickup_ties_and_octave_spans_survive_each_melody_entry(self):
|
||||
document = source(tests.test_musicxml.xml_note(duration=1, kind='quarter', extra='<tie type="start"/>'), tests.test_musicxml.xml_note(extra='<tie type="stop"/>'))
|
||||
document['staves'][0]['octave_shifts'] = [{'id': 'pickup-shift', 'start': generator.model.rational(0), 'end': generator.model.rational(1), 'octaves': -1}]
|
||||
for part, boundaries in (('vocal', (1, 5)), ('electric', (2,))):
|
||||
with self.subTest(part=part):
|
||||
result = generator.arrange.generate.arrange_document(document, part, rhythms=tests.rhythm_fixtures.library())
|
||||
generator.model.validate_document(result)
|
||||
measures = result['staves'][0]['measures']
|
||||
self.assertEqual(len(measures), 7)
|
||||
for boundary in boundaries:
|
||||
pickup = generator.score.measure_notes(measures[boundary - 1])[-1]
|
||||
downbeat = generator.score.measure_notes(measures[boundary])[0]
|
||||
self.assertEqual(generator.model.fraction(pickup['onset']), 3)
|
||||
self.assertEqual(pickup['pitches'][0]['tie_to'], downbeat['pitches'][0]['id'])
|
||||
self.assertEqual(downbeat['pitches'][0]['tie_from'], pickup['pitches'][0]['id'])
|
||||
self.assertTrue(any(generator.model.fraction(s['start']) == boundary * 4 - 1 and generator.model.fraction(s['end']) == boundary * 4 for s in result['staves'][0]['octave_shifts']))
|
||||
self.assertTrue(generator.playback.compile_timeline(result)['events'])
|
||||
def test_pickup_truncates_outgoing_music_without_discarding_earlier_notes(self):
|
||||
opening = '<note><rest/><duration>1</duration><type>quarter</type></note>' + tests.test_musicxml.xml_note('D', 3, 'half', extra='<dot/><lyric><text>doo</text></lyric>')
|
||||
document = source(opening)
|
||||
pickup, destination = copy.deepcopy(document['staves'][0]['measures'])
|
||||
before = copy.deepcopy(destination['voices'][0]['notes'][0])
|
||||
start = generator.arrange.pickup.splice(destination, pickup)
|
||||
self.assertEqual(start, 1)
|
||||
notes = generator.score.measure_notes(destination)
|
||||
self.assertEqual(len(notes), 2)
|
||||
self.assertEqual(notes[0]['id'], before['id'])
|
||||
self.assertEqual(notes[0]['pitches'][0]['id'], before['pitches'][0]['id'])
|
||||
self.assertEqual(generator.model.fraction(notes[0]['duration']), 1)
|
||||
self.assertEqual(notes[0]['written']['type'], 'quarter')
|
||||
self.assertEqual(notes[1]['lyrics'][0]['syllables'][0]['text'], 'doo')
|
||||
self.assertEqual([generator.model.fraction(n['onset']) for n in notes], [0, 1])
|
||||
document['staves'][0]['measures'] = [destination]
|
||||
generator.model.validate_document(document)
|
||||
def test_pickup_harmonies_are_planned_for_every_part_at_the_same_offset(self):
|
||||
document = source(tests.test_parts.xml_harmony('F') + tests.test_musicxml.xml_note('F', 1, 'quarter'))
|
||||
prepared = generator.parts.prepare_document(document)
|
||||
plans = generator.arrange.generate.plan_arrangement(prepared, tests.rhythm_fixtures.library())
|
||||
for part in generator.parts.PART_TYPES:
|
||||
result = generator.arrange.generate._arrange_document(prepared, part, plans)
|
||||
generator.model.validate_document(result)
|
||||
for index in (0, 1, 4):
|
||||
chords = result['staves'][0]['measures'][index]['harmonies']
|
||||
self.assertEqual([(generator.model.fraction(h['onset']), h['root']['letter']) for h in chords], [(0, 'c'), (3, 'f')])
|
||||
self.assertIn(fractions.Fraction(3), [event.onset for event in plans[0].acoustic_upper[0].events])
|
||||
def test_pickup_tuplets_keep_unique_identities_across_iterations(self):
|
||||
notes = ''
|
||||
for index, pitch in enumerate(('C', 'D', 'E')):
|
||||
tuplet = '<notations><tuplet type="start"/></notations>' if index == 0 else '<notations><tuplet type="stop"/></notations>' if index == 2 else ''
|
||||
notes += tests.test_musicxml.xml_note(pitch, 1, 'eighth', extra='<time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes><normal-type>eighth</normal-type></time-modification>' + tuplet)
|
||||
document = source(notes, divisions=3)
|
||||
for part in ('vocal', 'electric'):
|
||||
result = generator.arrange.generate.arrange_document(document, part, rhythms=tests.rhythm_fixtures.library())
|
||||
generator.model.validate_document(result)
|
||||
spans = result['staves'][0]['tuplets']
|
||||
self.assertEqual(len(spans), 2 if part == 'vocal' else 1)
|
||||
self.assertEqual(len({s['id'] for s in spans}), len(spans))
|
||||
for span in spans:
|
||||
members = [n for m in result['staves'][0]['measures'] for n in generator.score.measure_notes(m) if span['id'] in n.get('tuplet_ids', [])]
|
||||
self.assertEqual(len(members), 3)
|
||||
self.assertEqual([generator.model.fraction(n['onset']) for n in members], [3, fractions.Fraction(10, 3), fractions.Fraction(11, 3)])
|
||||
def test_overlong_pickup_is_rejected_with_measure_context(self):
|
||||
document = source(tests.test_musicxml.xml_note(duration=4), '<attributes><time><beats>3</beats><beat-type>4</beat-type></time></attributes>' + tests.test_musicxml.xml_note(duration=3, kind='half', extra='<dot/>'))
|
||||
with self.assertRaisesRegex(ValueError, 'measure 2: pickup is longer'):
|
||||
generator.arrange.generate.arrange_document(document, 'vocal', rhythms=tests.rhythm_fixtures.library())
|
||||
@@ -33,6 +33,18 @@ def shape(event):
|
||||
return tuple(sorted((p['tablature']['string'], p['tablature']['fret']) for p in event['pitches']))
|
||||
|
||||
class ElectricVoicingTests(unittest.TestCase):
|
||||
def test_mixed_pickup_bar_links_each_role_to_its_original_passage(self):
|
||||
chord = harmony('C7')
|
||||
pickup = note('pickup', 3, chord, (2, 3, 1, 3), melody=True)
|
||||
outgoing = note('outgoing', 0, chord, (5, 3, 5, 3))
|
||||
target = note('target', 0, chord, (10, 9, 11, 8))
|
||||
mixed = measure([outgoing, pickup])
|
||||
source = [measure([], [chord]), measure([], [harmony('G7')]), measure([], [chord])]
|
||||
origins = [generator.electric.MeasureOrigin(fractions.Fraction(8), 'accompaniment', mixed, end=fractions.Fraction(3)), generator.electric.MeasureOrigin(fractions.Fraction(-3), 'melody', mixed, start=fractions.Fraction(3)), generator.electric.MeasureOrigin(fractions.Fraction(0), 'accompaniment', measure([target]))]
|
||||
links = generator.electric.link_voicings(source, origins)
|
||||
self.assertEqual(len(links), 1)
|
||||
self.assertIs(links[0].target, target)
|
||||
self.assertEqual(links[0].references, (pickup,))
|
||||
def test_corresponding_melody_shapes_change_within_one_harmony(self):
|
||||
chord = harmony('C7')
|
||||
first = note('melody-1', 1, chord, (2, 3, 1, 3), melody=True, duration=2)
|
||||
|
||||
@@ -49,6 +49,7 @@ class EngravingTests(unittest.TestCase):
|
||||
def assert_paired_pages(self, layout):
|
||||
self.assertTrue(all(len(page.systems) == 2 for page in layout.pages[:-1]))
|
||||
self.assertIn(len(layout.pages[-1].systems), (1, 2))
|
||||
self.assertEqual(len({system.scale for page in layout.pages for system in page.systems}), 1)
|
||||
for page in layout.pages:
|
||||
self.assertEqual(len({system.scale for system in page.systems}), 1)
|
||||
for system in page.systems:
|
||||
@@ -59,6 +60,34 @@ class EngravingTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(max(x for p in lines for x, _ in p.points), page.width - layout.style.margin_x - 1)
|
||||
for first, second in zip(page.systems, page.systems[1:]):
|
||||
self.assertGreaterEqual(second.box.top - first.box.bottom, layout.style.system_gap - 0.001)
|
||||
def test_titles_sections_and_tempos_do_not_change_size_between_pages(self):
|
||||
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([tests.test_musicxml.xml_note()] * 40))
|
||||
source['metadata']['composer'] = 'Composer'
|
||||
for index in (0, 9, 18, 27):
|
||||
measure = source['staves'][0]['measures'][index]
|
||||
measure['section_label'] = f'Section {index}'
|
||||
measure['tempos'] = [{'onset': generator.model.rational(fractions.Fraction(1, 2)), 'written': {'type': 'quarter', 'dots': 0}, 'bpm': generator.model.rational(124)}]
|
||||
layout = self.layout(source)
|
||||
self.assertGreater(len(layout.pages), 2)
|
||||
self.assert_paired_pages(layout)
|
||||
self.assertEqual(layout.pages[0].systems[0].scale, 1)
|
||||
self.assertEqual(self.primitives(layout, 'tempo'), [])
|
||||
self.assertEqual(len(self.primitives(layout, 'section')), 4)
|
||||
self.assertEqual(len(self.primitives(layout, 'subtitle')), len(layout.pages))
|
||||
without_tempos = copy.deepcopy(source)
|
||||
for measure in without_tempos['staves'][0]['measures']:
|
||||
measure.pop('tempos', None)
|
||||
self.assertEqual(layout, self.layout(without_tempos))
|
||||
self.assertEqual(generator.playback.compile_timeline(source), generator.playback.compile_timeline(without_tempos))
|
||||
def test_long_credits_wrap_without_overflow_or_page_specific_sizing(self):
|
||||
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([tests.test_musicxml.xml_note()] * 32))
|
||||
source['metadata']['composer'] = 'A moderately long composer credit ' * 6
|
||||
layout = self.layout(source)
|
||||
self.assert_paired_pages(layout)
|
||||
header = [p for p in layout.pages[0].primitives if p.role in ('title', 'subtitle')]
|
||||
self.assertGreater(len(header), 2)
|
||||
for page in layout.pages:
|
||||
self.assertEqual(header, [p for p in page.primitives if p.role in ('title', 'subtitle')])
|
||||
def test_guitar_pages_always_pair_systems_including_tall_passages(self):
|
||||
content = tests.test_musicxml.xml_note('E', octave=5)
|
||||
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([content] * 32))
|
||||
|
||||
Reference in New Issue
Block a user