"""The held recording tail has exact score time and no vocal attacks.""" import generator.arrange.form import generator.composition import generator.source import generator.validation import copy import fractions import pathlib import unittest import generator.acoustic import generator.arrange.generate import generator.fretboard import generator.harmony import generator.model import generator.musicxml import generator.parts import generator.performance import generator.score import tests.solo_fixtures class ArrangementEnding(unittest.TestCase): def test_current_sources_end_on_effective_harmony_with_one_attack(self): for name, expected in (('all-of-me', 'GM6'), ('autumn_leaves', 'Dm')): source = generator.musicxml.load_musicxml(pathlib.Path('data/songs') / (name + '.xml')) before = copy.deepcopy(source) for part in ('acoustic', 'electric', 'vocal'): result = generator.arrange.generate.arrange_document(source, part) measures = result['staves'][0]['measures'] tail = measures[-3:] self.assertEqual(len(measures), 171) self.assertEqual([m['number'] for m in tail], ['169', '170', '171']) self.assertEqual(generator.harmony.symbol(tail[0]['harmonies'][0]), expected) self.assertEqual([m['section_label'] for m in tail], ['Ending', '', '']) self.assertTrue(all(m['ending_sustain'] for m in tail)) notes = [n for m in tail for n in generator.score.measure_notes(m)] attacks = [n for n in notes if n['pitches'] and not any(p.get('tie_from') for p in n['pitches'])] self.assertEqual(len(attacks), 0 if part == 'vocal' else 1) if part == 'vocal': self.assertTrue(all(n['kind'] == 'rest' and not n.get('lyrics') for n in notes)) else: self.assertEqual(sum(generator.model.fraction(n['duration']) for n in notes if n['pitches']), 12) self.assertEqual([p['tablature'] for p in notes[0]['pitches']], [p['tablature'] for p in notes[-1]['pitches']]) if part == 'acoustic': self.assertIn(tuple(p['tablature']['string'] for p in attacks[0]['pitches']), ((5, 4, 3, 2), (6, 4, 3, 2))) timing = generator.performance.timing(result) self.assertEqual(generator.model.fraction(timing['sustain_start']), 672) self.assertEqual(generator.model.fraction(timing['duration']), 684) self.assertEqual(len(timing['clicks']), 672) generator.validation.validate_document(result) generator.fretboard.validate_document(result) self.assertEqual(source, before) def test_ending_uses_final_meter_and_does_not_extend_solo_windows(self): for meter in (3, 5): source = tests.solo_fixtures.song(count=2, meter=meter, chords=('C7', 'FM7')) prepared = generator.parts.prepare_document(source) for part in ('acoustic', 'electric'): role = part + '_accompaniment' result = generator.composition.generate_role(generator.source.SourceAnalysis.read(prepared), role, pathlib.Path('data')) tail = result['document']['staves'][0]['measures'][-3:] self.assertEqual([generator.model.fraction(m['duration']) for m in tail], [meter] * 3) windows = generator.arrange.form.windows(prepared, part) self.assertEqual(windows[-1][2], role) self.assertEqual(windows[-1][1] - windows[-1][0], 3 * meter) self.assertEqual(sum(b - a for a, b, r in windows if r == part + '_solo'), 2 * meter) def test_missing_or_explicitly_absent_final_chord_is_not_invented(self): for no_chord in (False, True): source = tests.solo_fixtures.song(count=1, chords=('C7',)) last = source['staves'][0]['measures'][-1] if no_chord: last['harmonies'][0]['kind'] = 'none' else: last['harmonies'] = [] with self.assertRaisesRegex(ValueError, 'final performed harmony'): generator.arrange.generate.arrange_document(source, 'acoustic') def test_partial_combination_and_short_overlap_fail_after_tie_merging(self): source = tests.solo_fixtures.song(count=1, chords=('C7',)) doc = generator.arrange.generate.arrange_document(source, 'acoustic') tail = copy.deepcopy(doc) tail['staves'][0]['measures'] = tail['staves'][0]['measures'][-3:] generator.acoustic.validate_comping(tail) first = generator.score.measure_notes(tail['staves'][0]['measures'][0])[0] bass = first['pitches'][0] bass.pop('tie_to') bass['duration'] = generator.model.rational(1) with self.assertRaises(ValueError): generator.acoustic.validate_comping(tail) def test_ending_markers_cannot_interrupt_the_main_score(self): source = tests.solo_fixtures.song(count=3, chords=('C7',)) source['staves'][0]['measures'][0]['ending_sustain'] = True with self.assertRaisesRegex(ValueError, 'contiguous'): generator.performance.timing(source) if __name__ == '__main__': unittest.main()