Files
product-vamp/tests/test_solo_lead_tunes.py
T
2026-10-04 09:05:25 -05:00

148 lines
9.2 KiB
Python

"""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()