import copy import dataclasses from fractions import Fraction as F import unittest import generator.arrange.generate import generator.arrange.form import generator.model import generator.parts import generator.solo.arrangement from generator.solo import analysis, profiles, placement, themes, types, harmonic, gesture_rhythm, phrasing from tests.test_solo import song from tests.test_solo_development import note from tests.test_solo_passages import choices import tests.rhythm_fixtures def events(measure): return tuple((generator.model.fraction(n['onset']), generator.model.fraction(n['duration']), tuple((p['letter'],p['alter']['numerator'],p['octave'],p['tablature']['string'],p['tablature']['fret']) for p in n['pitches'])) for v in measure['voices'] for n in v['notes']) class ReusableAccompaniment(unittest.TestCase): def test_corresponding_bars_repeat_rhythm_pitch_and_fingering(self): source=song(count=12,chords=('CM7','Dm7','G7')) library=tests.rhythm_fixtures.library() prepared=generator.parts.prepare_document(source) plans=generator.arrange.generate.plan_arrangement(prepared,library) for part in ('acoustic','electric'): result=generator.arrange.generate._arrange_document(prepared,part,plans) seen={};offset=0;checked=0 sections=generator.arrange.form.full_arrangement_sections(source['staves'][0]['measures'],part) for section in sections: for j,m in enumerate(result['staves'][0]['measures'][offset:offset+section.measure_count]): if section.role!='accompaniment':continue key=section.source_measure_index+j if key in seen: self.assertEqual(events(m),seen[key]);checked+=1 seen[key]=events(m) self.assertFalse(any('tie_from' in p or 'tie_to' in p for v in m['voices'] for n in v['notes'] for p in n['pitches'])) offset+=section.measure_count self.assertGreater(checked,12) generator.model.validate_document(result) def test_excerpt_rhythms_retain_form_phase(self): source=generator.parts.prepare_document(song(count=13,chords=('CM7',))) plans=generator.arrange.generate.plan_arrangement(source,tests.rhythm_fixtures.library()) for lane in ('acoustic_bass','acoustic_upper','electric'): full=getattr(plans[1],lane) self.assertEqual(getattr(plans[0],lane),full[-8:]) self.assertEqual(getattr(plans[6],lane),full[-8:]) self.assertEqual(getattr(plans[3],lane),full) self.assertEqual(getattr(plans[4],lane),full) class SoloInstrumentAndIdentity(unittest.TestCase): def test_role_ranges_and_no_open_strings(self): acoustic=profiles.for_part('acoustic');electric=profiles.for_part('electric') self.assertEqual(acoustic.register,(41,76));self.assertEqual(electric.register,(41,76)) self.assertTrue(acoustic.permits(41));self.assertTrue(electric.permits(50)) self.assertTrue(acoustic.permits(72)) for profile in (acoustic,electric): for midi in range(40,78): for p in placement.positions(midi,profile): self.assertIn(p.string_number,profile.strings) self.assertTrue(1<=p.fret<=12) self.assertTrue(any(p.string_number==1 for p in placement.positions(65,electric))) self.assertTrue(any(p.string_number==1 for p in placement.positions(65,acoustic))) def test_soft_overlapping_registers_do_not_inherit_a_mandatory_octave(self): source=analysis.analyze(generator.parts.prepare_document(song(count=4,chords=('CM7',)))) acoustic=profiles.for_part('acoustic',source);electric=profiles.for_part('electric',source) self.assertLess(electric.reference-acoustic.reference,12) self.assertEqual(acoustic.melody_range,electric.melody_range) self.assertEqual(acoustic.cost(acoustic.reference+4),0) self.assertGreater(acoustic.cost(acoustic.reference+9),0) def test_return_keeps_opening_instead_of_later_largest_turn(self): notes=tuple(note(i,i+1,p,event_id=str(i)) for i,p in enumerate(('d','f','g','f','c','b'))) home=types.RealizedMotif('home','seed',notes,F(6),'introduce','contour',signature=notes[:4]) reference=themes.home_reference(home,'return') self.assertEqual(reference.notes,notes[:4]) self.assertEqual(reference.treatment,'return') self.assertEqual(len(themes.home_reference(home,'develop_contour').notes),3) def test_endings_distinguish_local_cadence_and_handoff(self): harmony=harmonic.Harmony(choices(('Em7b5','A7','Dm','Dm'))) self.assertEqual(phrasing.ending_effect(F(2),harmony),'local_settle') self.assertEqual(phrasing.ending_effect(F(9),harmony),'cadential_arrival') self.assertEqual(phrasing.ending_effect(F(15),harmony,True),'handoff') anchor=types.Anchor('comma',F(1),F(1),note(0,1,'g').pitch,frozenset({7}),(41,76),F(2),'terminal','Em7b5',hold_until=F(3)) self.assertEqual(set(phrasing.next_destination(anchor,harmony)),{2,5,9}) def test_rhythm_revision_connects_without_changing_attacks_or_arrival(self): notes=(note(0,1,'g',event_id='a'),note(F(3,2),2,'e',event_id='b'),note(F(5,2),3,'g',event_id='c'),note(3,6,'e',event_id='end')) anchor=types.Anchor('end',F(3),F(3),notes[-1].pitch,frozenset({4}),(41,76),F(3),'terminal','C7',hold_until=F(6)) draft=types.MelodicDraft('gesture',F(),F(6),notes,(),(),(),(anchor,),None,'develop',sustained_tails=(types.SustainedTail(F(3),F(6),64),)) revised,edits=gesture_rhythm.revise(draft,harmonic.Harmony(choices(('C7','C7')))) self.assertTrue(edits) self.assertEqual(tuple(n.pitch for n in revised.notes),tuple(n.pitch for n in notes)) self.assertEqual(revised.notes[-1],notes[-1]) self.assertLess(gesture_rhythm.fragmentation(revised.notes),gesture_rhythm.fragmentation(notes)) protected=dataclasses.replace(draft,protected=tuple(n.event_id for n in notes)) self.assertEqual(gesture_rhythm.revise(protected,harmonic.Harmony(choices(('C7','C7'))))[0].notes,notes) if __name__=='__main__':unittest.main()