Files
product-vamp/tests/test_solo_phrasing.py
T
khodowany f234dd6d51 Save
2026-10-04 15:51:18 -05:00

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