"""Production source variations, strict scale permission and paired interpretation.""" import copy import dataclasses from fractions import Fraction as F import pathlib import unittest from generator.solo import analysis, composer, drafts, harmonic, leads, placement, realization, scoring, types import generator.fretboard import generator.model import tests.test_solo as fixture import tests.test_solo_development as notes_fixture import tests.test_solo_passages as passage_fixture import tests.scale_fixtures import tests.rhythm_fixtures import tests.solo_fixtures class SourceVariationTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.document=fixture.song(count=8,chords=('CM7',)) cls.context=realization.SearchContext() cls.solo=cls.compose(cls.document,cls.context) @classmethod def compose(cls,document,context=None,end=32,part='acoustic'): return composer.compose(analysis.analyze(document),document['staves'][0]['measures'],part,F(end), scales=tests.scale_fixtures.library(),sequences=tests.solo_fixtures.library(),rhythms=tests.rhythm_fixtures.library(),context=context,mode='paraphrase') def test_production_is_traceable_to_complete_source_phrases(self): self.assertTrue(self.solo.phrases) for p in self.solo.phrases: self.assertIsNotNone(p.draft.source_phrase) self.assertTrue(p.draft.source_links) self.assertTrue(p.edits) self.assertGreater(len(p.draft.source_phrase.notes),8) self.assertTrue(drafts.contract_valid(p.draft,composer.phrase_notes(p))[0]) self.assertTrue(all(n.sources for n in p.draft.notes)) def test_same_input_and_cache_preserve_complete_music(self): other=self.compose(self.document,self.context) self.assertEqual(self.solo.notes(),other.notes()) self.assertGreater(self.context.diagnostics['cache_hits'],0) def test_source_change_drives_music_with_fixed_harmony(self): changed=copy.deepcopy(self.document) for m in changed['staves'][0]['measures']: for n in m['voices'][0]['notes']: n['pitches'][0]['letter']='g' if n['pitches'][0]['letter']=='c' else n['pitches'][0]['letter'] other=self.compose(changed) self.assertNotEqual(leads.signature(self.solo.notes()),leads.signature(other.notes())) self.assertNotEqual(self.solo.phrases[0].initial.notes[0].pitch.number()%12,other.phrases[0].initial.notes[0].pitch.number()%12) def test_source_ties_and_rests_are_retained_before_variation(self): melody=(notes_fixture.note(0,5,'c'),notes_fixture.note(6,9,'e'),notes_fixture.note(9,12,'g'),notes_fixture.note(12,16,'c')) melody=tuple(dataclasses.replace(n,sources=(f's{i}',)) for i,n in enumerate(melody)) source=analysis.source_phrases(melody,F(),F(16),F(4)) self.assertEqual(tuple(n for p in source for n in p.notes)[0].end,5) self.assertTrue(any((F(5),F(6)) in p.rests for p in source)) def test_strict_map_rejects_chromatic_labels(self): harmony=harmonic.Harmony(passage_fixture.choices(('C7',))) approach=dataclasses.replace(notes_fixture.note(F(5,2),3,'f',1),event_id='approach') target=dataclasses.replace(notes_fixture.note(3,4,'g'),event_id='target') contract=types.HarmonicTreatment('removed','chromatic_approach',66,F(5,2),F(3),67,F(3),F(),'approach','target') self.assertFalse(harmony.evaluate((approach,target),complete=True).valid) self.assertFalse(harmony.evaluate((approach,target),(contract,),complete=True).valid) def test_mapped_accidentals_and_half_steps_remain_valid(self): scale=tests.scale_fixtures.library().scale('harmonic_minor',1,'D') choices=passage_fixture.choices(('A7','Dm7'),duration=2,scale=scale) harmony=harmonic.Harmony(choices) notes=(notes_fixture.note(0,F(3,2),'e'),notes_fixture.note(F(3,2),2,'c',1),notes_fixture.note(2,4,'d')) self.assertTrue(harmony.evaluate(notes,complete=True).valid) def test_partition_does_not_change_sounding_evaluation(self): phrase=self.solo.phrases[0] whole=phrase.draft.notes;split=[] for n in whole: middle=(n.start+n.end)/2 split.extend((dataclasses.replace(n,end=middle),dataclasses.replace(n,start=middle,continuation=True))) harmony=harmonic.Harmony(self.solo.choices) self.assertEqual(harmony.evaluate(whole,complete=True),harmony.evaluate(split,complete=True)) self.assertEqual(scoring.features(phrase.plan,whole,self.solo.choices),scoring.features(phrase.plan,split,self.solo.choices)) self.assertTrue(drafts.contract_valid(phrase.draft,split)[0]) def test_low_register_and_tied_placement(self): low=dataclasses.replace(notes_fixture.note(0,2,'a'),pitch=types.Pitch('a',F(),2)) following=dataclasses.replace(low,start=2,end=4,continuation=True) placed=placement.place((low,following),F(120)) self.assertEqual(placed[1][0].position.string_number,6) self.assertEqual(placed[1][0].position,placed[1][1].position) self.assertEqual({p.string_number for number in range(41,77) for p in placement.positions(number)},set(range(1,7))) def test_pair_metric_detects_octave_copy(self): shifted=dataclasses.replace(self.solo,phrases=tuple(dataclasses.replace(p,decisions=tuple(dataclasses.replace(d,notes=leads.shift(d.notes,1)) for d in p.decisions)) for p in self.solo.phrases)) self.assertEqual(leads.similarity(self.solo,shifted),1) callback=dataclasses.replace(shifted,phrases=tuple(dataclasses.replace(p,plan=dataclasses.replace(p.plan,relationship='callback')) for p in shifted.phrases)) self.assertEqual(leads.similarity(self.solo,callback),0) def test_later_arrivals_compare_complete_routes_with_different_holds(self): notes=tuple(dataclasses.replace(notes_fixture.note(a,b,k),event_id=str(a),sources=(str(a),)) for a,b,k in ((0,1,'g'),(1,2,'e'),(2,8,'c'))) source=types.SourcePhrase('ending',F(),F(8),notes,(),'family') harmony=harmonic.Harmony(passage_fixture.choices(('C7','C7'))) options=leads.elaborate(notes,(),harmony,tests.solo_fixtures.library(),tests.rhythm_fixtures.library(),'4/4',source) self.assertEqual({r.destination.end-r.destination.start for _,_,r in options},{F(1,2),F(1),F(3,2),F(2)}) for actual,_,route in options: self.assertTrue(harmony.evaluate(actual,complete=True).valid) self.assertEqual(actual[-1],route.destination) self.assertEqual((actual[0].start,actual[-1].end),(0,8)) def test_repeated_source_cadence_is_not_accidental_monotony(self): first=self.solo.phrases[0] changed=dataclasses.replace(first,draft=dataclasses.replace(first.draft, source_phrase=dataclasses.replace(first.draft.source_phrase,family='unrelated'))) self.assertAlmostEqual(leads.transition(first,changed)-leads.transition(first,first),.15) def test_space_and_vocal_return_are_preserved(self): silent=self.compose(fixture.song(count=8,chords=())) self.assertFalse(silent.notes()) shortened=self.compose(self.document,end=F(29)) self.assertTrue(all(n.end<=29 for n in shortened.notes())) composer.validate(shortened) def test_batch_selection_preserves_independent_source_variations(self): a=analysis.analyze(self.document) pair=composer.compose_pair(a,{part:(self.document['staves'][0]['measures'],F(32),None) for part in ('acoustic','electric')}, scales=tests.scale_fixtures.library(),sequences=tests.solo_fixtures.library(),rhythms=tests.rhythm_fixtures.library(),mode='paraphrase') for solo in pair: alone=self.compose(self.document,part=solo.part) self.assertEqual(solo.notes(),alone.notes()) self.assertNotIn('pairs_compared',solo.diagnostics) for solo in pair: composer.validate(solo) for phrase in solo.phrases: heard={n.event_id:n for n in composer.phrase_notes(phrase)} for route in phrase.routes: self.assertEqual(route.notes[-1].pitch,route.destination.pitch) for note in route.notes: self.assertEqual((heard[note.event_id].pitch,heard[note.event_id].start,heard[note.event_id].end),(note.pitch,note.start,note.end)) def test_pickup_is_brought_inside_window_without_touching_previous_section(self): melody=(notes_fixture.note(0,5,'g'),notes_fixture.note(5,6,'a'),notes_fixture.note(6,8,'g')) melody=tuple(dataclasses.replace(n,sources=(f's{i}',),event_id=f's{i}') for i,n in enumerate(melody)) pickup=tuple(dataclasses.replace(notes_fixture.note(i-3,i-2,name),sources=(f'p{i}',),event_id=f'p{i}') for i,name in enumerate(('c','d','e'))) source=types.SourcePhrase('pickup',F(),F(8),melody,(),'family',pickup) harmony=harmonic.Harmony(passage_fixture.choices(('C7','C7'))) options=[notes for notes,edits in leads.variations(melody,source,harmony) if edits[0].kind=='pickup_entrance'] self.assertTrue(options) self.assertEqual(options[0][0].start,0) self.assertEqual(options[0][3].start,3) self.assertEqual(options[0][3].end,5) self.assertTrue(harmony.evaluate(options[0],complete=True).valid) def test_occupancy_is_not_an_objective_and_active_silence_is_invalid(self): plan=dataclasses.replace(notes_fixture.plan(),end=F(4),release=F(),passages=(),treatment='source_variation') choices=passage_fixture.choices(('C7',)) for end in (F(1),F(4)): self.assertEqual(scoring.features(plan,(notes_fixture.note(0,end,'e'),),choices)['occupancy'],0) self.assertFalse(scoring.feasible(plan,(),choices,F(4))) if __name__=='__main__':unittest.main()