"""Independent musical fixtures; the four optional user scores are never read.""" import copy import dataclasses import json from fractions import Fraction as F from pathlib import Path import tempfile from types import SimpleNamespace import unittest from audition import reference_review, solo_compare from generator import vocabulary, production from generator.solo import export, gesture_rhythm, harmonic, notation, placement, routes, themes, types from tests.test_solo import song from tests.test_solo_development import note from tests.test_solo_passages import choices from tests import solo_fixtures def xml(notes, *, transpose=False, leadin=''): return 'Vocal'+leadin+'60G2'+('0-1' if transpose else '')+'Cmajor-seventh'+notes+'' def pitched(duration, tie='', octave=5): return f'C{octave}{duration}{tie}quarter' class ComparisonTests(unittest.TestCase): def review(self,directory,name,content,**kwargs): path=Path(directory)/name;path.write_text(content) return reference_review.read(path,'fixture/acoustic',**kwargs) def test_ties_not_reattacks_and_one_guitar_transposition(self): with tempfile.TemporaryDirectory() as root: a=self.review(root,'actual.xml',xml(pitched(24),transpose=True),form_start=1) b=self.review(root,'reference.xml',xml(pitched(12,'')+pitched(12,'')),form_start=1,pitch_convention='guitar-written') self.assertTrue(solo_compare.compare(a,b)['exact_musical_match']) self.assertEqual(a['sounding_events'][0]['sounding_midi'],'60') repeated=self.review(root,'repeat.xml',xml(pitched(12)+pitched(12)),form_start=1,pitch_convention='guitar-written') self.assertFalse(solo_compare.compare(a,repeated)['exact_musical_match']) def test_leadin_is_reported_without_harmony_or_alignment_padding(self): with tempfile.TemporaryDirectory() as root: prelude='6'+pitched(6)+'18' a=self.review(root,'a.xml',xml(pitched(24),transpose=True),form_start=1) b=self.review(root,'b.xml',xml(pitched(24),leadin=prelude),form_start=2,pitch_convention='guitar-written') result=solo_compare.compare(a,b) self.assertEqual(result['differences'][0]['time'],'-4') self.assertEqual(result['exact_matching_attacks'],1) self.assertIn('uncertainty',b['notes'][0]['harmonic_spans'][0]) def test_unplayable_pitch_is_reported_not_folded(self): with tempfile.TemporaryDirectory() as root: r=self.review(root,'high.xml',xml(pitched(24,octave=7)),form_start=1,pitch_convention='guitar-written') checked=solo_compare.playability(r) self.assertEqual(checked['conflicts'][0]['sounding_midi'],84) self.assertEqual(r['notes'][0]['pitch']['octave'],7) def test_silent_reference_leadin_remains_in_audition_clock(self): with tempfile.TemporaryDirectory() as root: prelude='624' a=self.review(root,'actual.xml',xml(pitched(24),transpose=True),form_start=1) b=self.review(root,'reference.xml',xml(pitched(24),leadin=prelude),form_start=2,pitch_convention='guitar-written') self.assertEqual(solo_compare.audition_bounds(a,b),(F(-4),F(4))) def test_triplet_time_and_annotations_are_literal(self): with tempfile.TemporaryDirectory() as root: triplet=pitched(2).replace('quarter','eighth32') r=self.review(root,'thirds.xml',xml('Lead in'+triplet*3+'18'),form_start=1) self.assertEqual([n['start'] for n in r['sounding_events']],['0','1/3','2/3']) self.assertEqual(r['annotations'][0]['text'],'Lead in') def test_export_roundtrip_preserves_actual_sounding_events_and_tab(self): document=song(count=2,chords=('CM7',)) raw=(note(0,F(1,3),'c'),note(F(1,3),F(2,3),'d'),note(F(2,3),1,'e'),note(F(7,2),5,'g'),note(5,6,'g')) placed=placement.place(raw,F(120))[1] notation.replace_region(document,0,notation.measures(document['staves'][0]['measures'],placed,'export')) document['staves'][0]['measures'][0]['section_label']='Acoustic Solo' before=copy.deepcopy(document) with tempfile.TemporaryDirectory() as root: path=Path(root)/'fixture-solo1.xml' info=export.write_role({'role':'acoustic_solo','document':document,'trace':{'source':'fixture'}},path) loaded=reference_review.read(path,'fixture/acoustic',1) sounded=[(F(n['start']),F(n['end']),F(n['sounding_midi'])) for n in loaded['sounding_events']] self.assertEqual([(n.start,n.end,n.pitch.number()) for n in raw],sounded) from generator import score tab=lambda d:[p['tablature'] for m in d['staves'][0]['measures'] for n in score.measure_notes(m) for p in n['pitches']] self.assertEqual(tab(document),[n['tablature'] for n in loaded['notes'] if n['pitch']]) self.assertEqual(info['role'],'acoustic_solo') self.assertEqual(before,document) def test_export_manifests_are_not_discovered_as_runtime_scores(self): from generator import score document=song(count=1,chords=('CM7',)) with tempfile.TemporaryDirectory() as root: root=Path(root);(root/'analysis').mkdir() roles={} for part in ('acoustic','electric'): doc=copy.deepcopy(document) doc['staves'][0]['measures'][0]['section_label']=part.capitalize()+' Solo' roles[part+'_solo']={'role':part+'_solo','document':doc,'trace':{'source':'synthetic'}} (root/'analysis'/'fixture.roles').write_text(json.dumps(roles)) score.write_json(root/'score.json',document) export.export_tree(root) self.assertEqual(score.count_json_files(root),1) score.validate_generated_tree(root) self.assertTrue((root/'solo_xml'/'fixture-solo1.xml').is_file()) self.assertTrue((root/'solo_xml'/'fixture-solo2.xml').is_file()) class MusicalSelectionTests(unittest.TestCase): def test_cell_purpose_survives_loading_and_filters_eligibility(self): cells=tuple(vocabulary.rhythm_cells(120,'arrival')) self.assertTrue(cells) self.assertTrue(all(c.purpose=='arrival' for c in cells)) self.assertFalse(any(c.id=='short_answer' for c in cells)) def test_connecting_rhythm_prefers_groups_to_isolated_notes(self): joined=((F(),F(1,2)),(F(1,2),F(3,2)),(F(3,2),F(2))) fragmented=((F(),F(1,2)),(F(1),F(3,2)),(F(2),F(5,2))) self.assertLess(themes.rhythm_cost(joined,F(4),'connect'),themes.rhythm_cost(fragmented,F(4),'connect')) def test_complete_signature_boundary_can_cross_barline(self): home=SimpleNamespace(signature=(note(0,2),note(2,5))) source=SimpleNamespace(end=F(24));ending=SimpleNamespace(earliest=F(21)) self.assertEqual(themes.unit_end(source,F(8),home,ending,F(4)),13) def test_arpeggio_is_not_charged_for_each_nonstep_interval(self): chord=(note(0,F(1,2),'c'),note(F(1,2),1,'e'),note(1,F(3,2),'g')) steps=(note(0,F(1,2),'c'),note(F(1,2),1,'d'),note(1,F(3,2),'e')) self.assertEqual(routes.path_cost(chord),routes.path_cost(steps)) h=harmonic.Harmony(choices(('CM7',))) self.assertEqual(routes.path_family(chord,h)[0],'arpeggio') self.assertEqual(routes.path_family(steps,h)[0],'mapped') run=tuple(note(i,F(i+1),p) for i,p in enumerate(('c','d','e','f','g','a'))) self.assertEqual(routes.path_cost(run),0) oscillation=tuple(note(i,F(i+1),p) for i,p in enumerate(('c','d','c','d','c'))) self.assertGreater(routes.path_cost(oscillation),0) def test_data_driven_arpeggio_reaches_exact_destination_as_one_route(self): h=harmonic.Harmony(choices(('CM7',))) notes=(note(0,F(1,2),'c',event_id='first'),note(F(1,2),1,'c',event_id='middle'),note(1,F(3,2),'g',event_id='target')) paths=routes.candidates(notes,{'target'},h,solo_fixtures.library(),(41,76)) authored=[r for r in paths if any(s.startswith('chord-path:') for s in r.sources)] self.assertTrue(authored) self.assertTrue(any([n.pitch.number() for n in r.notes]==[60,64,67] for r in authored)) self.assertTrue(all(r.notes[-1].pitch==notes[-1].pitch for r in paths)) self.assertTrue(all(h.evaluate(r.notes).valid for r in paths)) def test_solo_vocabulary_changes_do_not_change_accompaniment_inputs(self): resources=vocabulary.load() changed=copy.deepcopy(resources) changed['melodic_vocabulary']['cells'].append({'id':'independent-test'}) for role in ('acoustic_accompaniment','electric_accompaniment','electric_melody','vocal_melody'): self.assertEqual(production.dependencies(role,resources),production.dependencies(role,changed)) self.assertNotEqual(production.dependencies('acoustic_solo',resources),production.dependencies('acoustic_solo',changed)) def test_cross_bar_arrival_cell_remains_one_sounding_duration(self): cell=next(c for c in vocabulary.rhythm_cells(120,'arrival') if c.id=='anticipated_sustain') self.assertTrue(any(a<4