115 lines
6.8 KiB
Python
115 lines
6.8 KiB
Python
"""Independent harmonic relationships; no optional reference reads."""
|
|
import dataclasses
|
|
from fractions import Fraction as F
|
|
import unittest
|
|
from generator.solo import gestures, harmonic, routes, types, expressions, placement, themes, realization
|
|
from tests.test_solo_passages import choices
|
|
from tests.test_solo_development import note
|
|
import tests.solo_fixtures
|
|
import tests.scale_fixtures
|
|
import tests.rhythm_fixtures
|
|
|
|
|
|
class JointArrivals(unittest.TestCase):
|
|
def fixture(self):
|
|
h=harmonic.Harmony(choices(('Dm7','G7'),scale=tests.scale_fixtures.library().scale('major',1,'C')))
|
|
ns=tuple(note(F(i,2),F(i+1,2),'d' if i<8 else 'g',event_id=str(i)) for i in range(16))
|
|
return h,ns
|
|
|
|
def test_anticipation_is_planned_before_route_and_requires_its_contract(self):
|
|
h,ns=self.fixture()
|
|
alternatives=list(gestures.proposals(ns,h,F(),F(8)))
|
|
p=next(p for p in alternatives if p.contracts and p.contracts[0].kind=='anticipation' and p.contracts[0].target.pitch.letter=='b' and p.contracts[0].target.end-p.contracts[0].target.start>=2)
|
|
result=routes.candidates(p.notes,(),h,tests.solo_fixtures.library(),(41,76),expressions=p.contracts)
|
|
self.assertTrue(result)
|
|
for r in result:
|
|
bound=p.bind(r.notes)
|
|
self.assertTrue(h.evaluate(r.notes,expressions=bound,complete=True).valid)
|
|
self.assertFalse(h.evaluate(r.notes,complete=True).valid)
|
|
self.assertLess(bound[0].target.start,F(4));self.assertGreater(bound[0].target.end,F(4))
|
|
self.assertIsNotNone(placement.place(r.notes,F(120)))
|
|
|
|
def test_approach_and_anticipation_share_exact_future_target(self):
|
|
h,ns=self.fixture()
|
|
base=next(p for p in gestures.proposals(ns,h,F(),F(8)) if p.contracts and p.contracts[0].kind=='anticipation')
|
|
proposals=list(gestures.approach(base,h,F()))
|
|
self.assertTrue(proposals)
|
|
realized=[]
|
|
for p in proposals:
|
|
for r in routes.candidates(p.notes,(),h,tests.solo_fixtures.library(),(41,76),expressions=p.contracts):
|
|
bound=p.bind(r.notes);self.assertTrue(h.evaluate(r.notes,expressions=bound,complete=True).valid)
|
|
self.assertFalse(h.evaluate(r.notes,expressions=bound[1:],complete=True).valid)
|
|
realized.append((r,bound))
|
|
self.assertTrue(realized)
|
|
r,contracts=realized[0];target=contracts[0].target
|
|
changed=tuple(dataclasses.replace(n,pitch=dataclasses.replace(n.pitch,octave=n.pitch.octave+1)) if n.event_id==target.event_id else n for n in r.notes)
|
|
self.assertFalse(h.evaluate(changed,expressions=contracts,complete=True).valid)
|
|
|
|
def test_on_time_and_delayed_arrivals_are_real_alternatives(self):
|
|
h,ns=self.fixture();ps=list(gestures.proposals(ns,h,F(),F(8)))
|
|
offsets={p.notes[next(i for i,n in enumerate(p.notes) if n.role=='arrival')].start-4 for p in ps if p.purpose.startswith('harmonic_arrival')}
|
|
self.assertEqual(offsets,{F(-1,2),F(),F(1,2)})
|
|
|
|
def test_proposal_cache_preserves_music_and_restores_each_callers_provenance(self):
|
|
h,ns=self.fixture()
|
|
direct=gestures._select(ns,h,F(),F(8))
|
|
cached=gestures.select(ns,h,F(),F(8))
|
|
self.assertEqual(direct,cached)
|
|
changed=tuple(dataclasses.replace(n,event_id='caller:'+n.event_id,sources=('new-source',)) for n in ns)
|
|
self.assertEqual(gestures.select(changed,h,F(),F(8)),gestures._select(changed,h,F(),F(8)))
|
|
self.assertEqual(len(h.gesture_cache),1)
|
|
|
|
def test_route_cache_preserves_contract_conditioned_realization(self):
|
|
h,ns=self.fixture()
|
|
p=next(p for p in gestures.proposals(ns,h,F(),F(8)) if p.contracts and p.contracts[0].kind=='anticipation')
|
|
direct=routes._planned_candidates(p.notes,(),h,(41,76),'test',p.contracts)
|
|
cached=routes.planned_candidates(p.notes,(),h,(41,76),'test',p.contracts)
|
|
self.assertTrue(cached)
|
|
self.assertEqual(cached,direct)
|
|
self.assertEqual(cached,routes.planned_candidates(p.notes,(),h,(41,76),'test',p.contracts))
|
|
|
|
def test_color_is_a_planned_route_destination_not_a_blanket_permission(self):
|
|
h=harmonic.Harmony(choices(('CM7','CM7'),scale=tests.scale_fixtures.library().scale('major',1,'C')))
|
|
ns=(note(0,1,'c',event_id='a'),note(1,3,'e',event_id='b'),note(3,4,'g',event_id='c'))
|
|
p=next(p for p in gestures.proposals(ns,h,F(),F(4)) if p.contracts and p.contracts[0].rule=='major_ninth' and p.contracts[0].events[0].end-p.contracts[0].events[0].start>=2)
|
|
rs=routes.candidates(p.notes,(),h,tests.solo_fixtures.library(),(41,76),expressions=p.contracts)
|
|
self.assertTrue(rs)
|
|
self.assertFalse(h.evaluate(rs[0].notes).valid)
|
|
self.assertTrue(h.evaluate(rs[0].notes,expressions=p.bind(rs[0].notes),complete=True).valid)
|
|
|
|
def test_independent_transpositions_and_harmonic_rhythms(self):
|
|
for tonic,symbols in [('C',('Dm7','G7')),('F',('Gm7','C7')),('G',('Am7','D7'))]:
|
|
for length in (3,4):
|
|
h=harmonic.Harmony(choices(symbols,duration=length,scale=tests.scale_fixtures.library().scale('major',1,tonic)))
|
|
ns=tuple(note(F(i,2),F(i+1,2),'g',event_id=str(i)) for i in range(length*4))
|
|
ps=[p for p in gestures.proposals(ns,h,F(),F(length*2)) if p.contracts and p.contracts[0].kind=='anticipation']
|
|
self.assertTrue(ps,(tonic,length))
|
|
self.assertTrue(any(routes.candidates(p.notes,(),h,tests.solo_fixtures.library(),(41,76),expressions=p.contracts) for p in ps),(tonic,length))
|
|
|
|
def test_closed_hold_cannot_be_consumed_by_a_joint_action(self):
|
|
h,ns=self.fixture();target=ns[-1]
|
|
ending=types.Anchor('close',F(6),F(6),target.pitch,frozenset({7}),(41,76),F(2),'terminal','G7',hold_until=F(8))
|
|
for p in gestures.proposals(ns,h,F(),F(8),ending):
|
|
self.assertFalse(any(n.role=='arrival' and n.end>6 for n in p.notes))
|
|
|
|
def test_minor_sixth_is_bounded_stable_color_not_required_neighbor_resolution(self):
|
|
h=harmonic.Harmony(choices(('Dm','Dm')))
|
|
n=note(0,3,'b',octave=3,event_id='color')
|
|
c=types.ExpressionContract('sixth','color','minor_sixth',(n,),n,'Dm')
|
|
self.assertTrue(h.evaluate((n,),expressions=(c,),complete=True).valid)
|
|
self.assertFalse(h.evaluate((n,),complete=True).valid)
|
|
too_long=dataclasses.replace(n,end=F(5));bad=dataclasses.replace(c,events=(too_long,),target=too_long)
|
|
self.assertFalse(h.evaluate((too_long,),expressions=(bad,),complete=True).valid)
|
|
|
|
def test_whole_tone_enclosure_neighbor_must_actually_be_mapped(self):
|
|
h=harmonic.Harmony(choices(('C7',),scale=tests.scale_fixtures.library().scale('major',5,'C')))
|
|
target=note(2,4,'g',event_id='target')
|
|
upper=note(1,F(3,2),'a',event_id='upper');lower=note(F(3,2),2,'f',alter=1,event_id='lower')
|
|
c=types.ExpressionContract('enclosure','enclosure','mapped_upper_lower',(upper,lower),target,'C7')
|
|
self.assertTrue(h.evaluate((upper,lower,target),expressions=(c,),complete=True).valid)
|
|
altered=harmonic.Harmony(choices(('C7',),scale=tests.scale_fixtures.library().scale('harmonic_minor',5,'C')))
|
|
self.assertFalse(altered.evaluate((upper,lower,target),expressions=(c,),complete=True).valid)
|
|
|
|
|
|
if __name__=='__main__':unittest.main()
|