134 lines
7.7 KiB
Python
134 lines
7.7 KiB
Python
import dataclasses
|
|
from fractions import Fraction as F
|
|
import unittest
|
|
|
|
from generator.solo import drafts, harmonic, phrasing, themes, types, leads, realization
|
|
from tests.test_solo_development import note
|
|
from tests.test_solo_passages import choices
|
|
from tests.test_solo_themes import melody
|
|
import tests.rhythm_fixtures
|
|
import tests.solo_fixtures
|
|
|
|
|
|
class SustainedPhrasing(unittest.TestCase):
|
|
def test_vocal_cutoff_does_not_strand_a_one_beat_final_phrase(self):
|
|
from generator.solo import analysis
|
|
from tests.test_solo import song
|
|
document=song(count=32,chords=('CM7',))
|
|
source=analysis.analyze(document)
|
|
harmony=harmonic.Harmony(choices(('CM7',)*32))
|
|
values=phrasing.window_options(source,document['staves'][0]['measures'],F(100),F(125),harmony,types.ContinuationState('',closures=5),limit=4)
|
|
self.assertEqual(values[0].end,125)
|
|
self.assertFalse(any(0<125-v.end<4 for v in values))
|
|
|
|
def test_production_cost_uses_complete_sounding_events_across_cells(self):
|
|
harmony=harmonic.Harmony(choices(('C7','C7')))
|
|
whole=(note(0,1,'g'),note(1,F(3,2),'f'),note(F(3,2),4,'e'))
|
|
split=(whole[0],whole[1],dataclasses.replace(whole[2],end=2),note(2,4,'e',continuation=True))
|
|
incoming=types.ContinuationState('')
|
|
from tests.test_composer_repair import evaluated
|
|
self.assertEqual(evaluated(whole,harmony),evaluated(split,harmony))
|
|
|
|
def test_route_cache_preserves_concrete_provenance_and_uncached_selection(self):
|
|
from generator.solo import routes,sequences,policy
|
|
harmony=harmonic.Harmony(choices(('C7',)))
|
|
notes=melody(((0,1,'g'),(1,2,'f'),(2,3,'e')))
|
|
library=tests.solo_fixtures.library()
|
|
raw=routes._candidates(notes,{'2'},harmony,library,(41,76))
|
|
expected=sequences.diverse(raw,
|
|
lambda r:(sum(abs(b.pitch.number()-a.pitch.number()) for a,b in zip(r.notes,r.notes[1:])),r.id),
|
|
lambda r:sequences.family(tuple(b.pitch.number()-a.pitch.number() for a,b in zip(r.notes,r.notes[1:]))),policy.DEFAULT.join_alternatives)
|
|
cached=routes.candidates(notes,{'2'},harmony,library,(41,76))
|
|
self.assertEqual(tuple((r.id,r.notes) for r in expected),tuple((r.id,r.notes) for r in cached))
|
|
renamed=tuple(dataclasses.replace(n,event_id='other'+n.event_id,sources=('other-source',)) for n in notes)
|
|
changed=routes.candidates(renamed,{'other2'},harmony,library,(41,76))
|
|
self.assertEqual(len(harmony.route_cache),1)
|
|
self.assertTrue(all(n.sources==('other-source',) for r in changed for n in r.notes))
|
|
self.assertTrue(all(n.event_id.startswith('other') for r in changed for n in r.notes))
|
|
|
|
def test_extended_closing_unit_has_complete_timing_beyond_two_bars(self):
|
|
values=themes.rhythm_windows(tests.rhythm_fixtures.library(),'4/4',F(12),realization.SearchContext())
|
|
self.assertTrue(values)
|
|
self.assertTrue(any(any(a>=8 for a,b in pattern) for pattern,_ in values))
|
|
for pattern,_ in values:
|
|
self.assertTrue(all(0<=a<b<=12 for a,b in pattern))
|
|
self.assertTrue(all(b<=c for (a,b),(c,d) in zip(pattern,pattern[1:])))
|
|
|
|
def test_two_bar_retrieval_uses_the_richer_one_bar_vocabulary(self):
|
|
values=themes.rhythm_windows(tests.rhythm_fixtures.library(),'4/4',F(8),realization.SearchContext())
|
|
self.assertTrue(any(b-a==F(1,2) for pattern,_ in values for a,b in pattern))
|
|
|
|
def test_endpoint_ranking_distinguishes_major_seventh_from_dominant(self):
|
|
major=harmonic.Harmony(choices(('CM7',)*4))
|
|
dominant=harmonic.Harmony(choices(('C7',)*4))
|
|
self.assertLess(phrasing.endpoint_cost(F(16),F(),F(128),0,major,F(4)),
|
|
phrasing.endpoint_cost(F(16),F(),F(128),0,dominant,F(4)))
|
|
|
|
def test_protected_tail_is_sounding_time_not_an_immutable_whole_event(self):
|
|
original=(note(0,4,'e',event_id='arrival'),)
|
|
tail=types.SustainedTail(F(2),F(4),64)
|
|
draft=types.MelodicDraft('held',F(),F(4),original,(),(),(),(),None,'close',sustained_tails=(tail,))
|
|
revised=(note(0,1,'g',event_id='a'),note(1,2,'f',event_id='b'),note(2,4,'e',event_id='c'))
|
|
self.assertTrue(drafts.contract_valid(draft,revised)[0])
|
|
tied=(*revised[:-1],note(2,3,'e',event_id='c'),note(3,4,'e',event_id='c',continuation=True))
|
|
self.assertTrue(drafts.contract_valid(draft,tied)[0])
|
|
repeated=(*revised[:-1],note(2,3,'e',event_id='c'),note(3,4,'e',event_id='d'))
|
|
self.assertFalse(drafts.contract_valid(draft,repeated)[0])
|
|
|
|
def test_planned_endings_have_varied_timing_and_full_chord_permission(self):
|
|
harmony=harmonic.Harmony(choices(('Gm7','C7','FM7','BbM7')))
|
|
source=types.SourcePhrase('phrase',F(),F(16),(note(0,1,'d'),),(),'')
|
|
options=phrasing.endings(source,types.ContinuationState(''), 'acoustic',harmony,F(4))
|
|
self.assertTrue(options)
|
|
self.assertGreater(len({(a.earliest,a.duration) for a in options}),1)
|
|
for anchor in options:
|
|
self.assertGreaterEqual(anchor.duration,2)
|
|
self.assertLessEqual(anchor.duration,4)
|
|
self.assertIn(anchor.pitch.number()%12,harmony.members(anchor.earliest,anchor.hold_until))
|
|
self.assertTrue(harmony.evaluate((types.Note(anchor.earliest,anchor.hold_until,anchor.pitch),),complete=True).valid)
|
|
|
|
def test_rhythmic_development_keeps_a_heard_turn_without_copying_its_timing(self):
|
|
original=melody(((0,1,'d'),(1,2,'f'),(2,3,'e')))
|
|
home=types.RealizedMotif('home','seed',original,F(3),'introduce','contour')
|
|
ref=themes.home_reference(home,'develop_contour')
|
|
context=realization.SearchContext()
|
|
patterns=themes.patterns(ref,F(),F(8),tests.rhythm_fixtures.library(),'4/4',context,home=home,operation='develop_contour')
|
|
self.assertGreater(len({p[:3] for p,_ in patterns}),1)
|
|
actual=(note(0,F(1,2),'d',event_id='0'),note(F(1,2),2,'f',event_id='1'),note(2,3,'e',event_id='2'))
|
|
feature=themes.feature(ref,actual,treatment='develop_contour')
|
|
from generator.solo.references import feature_error
|
|
self.assertEqual(feature_error(feature,actual),0)
|
|
|
|
def test_repetition_remembers_recent_figures_but_not_tie_splits(self):
|
|
original=melody(((0,1,'d'),(1,2,'f'),(2,3,'e'),(3,4,'f')))
|
|
old=phrasing.heard(original,'introduce')
|
|
repeated=tuple(dataclasses.replace(n,start=n.start+8,end=n.end+8) for n in original)
|
|
figure=phrasing.heard(repeated,'develop_contour')
|
|
self.assertGreater(phrasing.repetition(figure,(old,),F(4)),phrasing.repetition(figure,(),F(4)))
|
|
returned=dataclasses.replace(figure,operation='return')
|
|
self.assertGreater(phrasing.repetition(returned,(old,old,old),F(4)),phrasing.repetition(returned,(old,),F(4)))
|
|
hold=(note(0,4,'e'),)
|
|
split=(note(0,2,'e'),note(2,4,'e',continuation=True))
|
|
self.assertEqual(phrasing.heard(hold,'close'),phrasing.heard(split,'close'))
|
|
self.assertEqual(phrasing.repetition(phrasing.heard(hold,'close'),(),F(4)),0)
|
|
|
|
def test_high_electric_state_proposes_a_lower_goal(self):
|
|
high=melody(((0,1,'c'),(1,2,'e'),(2,3,'d')))
|
|
high=tuple(dataclasses.replace(n,pitch=dataclasses.replace(n.pitch,octave=5)) for n in high)
|
|
state=types.ContinuationState('',last=high[-1],history=(phrasing.heard(high,'develop_contour'),),register_peak=76)
|
|
self.assertLess(phrasing.register_goal(state,'electric',74),min(n.pitch.number() for n in high))
|
|
|
|
def test_internal_short_notes_can_be_elaborated_without_consuming_arrival(self):
|
|
notes=melody(((0,1,'g'),(1,2,'f'),(2,3,'g'),(3,5,'e')))
|
|
source=types.SourcePhrase('internal',F(),F(5),notes,(),'')
|
|
harmony=harmonic.Harmony(choices(('C7','C7')))
|
|
tails=(types.SustainedTail(F(3),F(5),64),)
|
|
options=leads.elaborate(notes,(),harmony,tests.solo_fixtures.library(),tests.rhythm_fixtures.library(),'4/4',source,sustain_entry=True,tails=tails)
|
|
self.assertTrue(options)
|
|
for actual,_,_ in options:
|
|
self.assertTrue(any(n.pitch.number()==64 and n.start<=3 and n.end>=5 for n in actual))
|
|
self.assertTrue(harmony.evaluate(actual,complete=True).valid)
|
|
|
|
|
|
if __name__=='__main__':unittest.main()
|