97 lines
5.3 KiB
Python
97 lines
5.3 KiB
Python
"""General, independently authored capabilities of the active action search."""
|
|
import dataclasses, unittest
|
|
from fractions import Fraction as F
|
|
from generator import model, vocabulary
|
|
from generator.scales import selection, timeline
|
|
from generator.solo import analysis, construction, harmonic, profiles, types, placement, operators
|
|
from tests import solo_fixtures as fixtures, rhythm_fixtures, scale_fixtures
|
|
|
|
|
|
def builder(symbols=('C7','FM7','Dm7','G7'),seed='independent',count=8):
|
|
source=fixtures.song(count=count,chords=symbols);a=analysis.analyze(source);ms=source['staves'][0]['measures']
|
|
end=sum(model.fraction(m['duration']) for m in ms)
|
|
cs=selection.select(timeline.read(source['staves'][0]),timeline.Solo(F(),end,'test'),scale_fixtures.library())
|
|
return construction.Builder(a,ms,harmonic.Harmony(cs,profiles.Instrument(reference=60)),fixtures.library(),rhythm_fixtures.library(),end,seed)
|
|
|
|
class PhraseConstruction(unittest.TestCase):
|
|
def test_candidates_include_open_motion_arrivals_and_joint_expression(self):
|
|
b=builder(count=2);values=b.operators.at(F())
|
|
self.assertTrue(any(a.kind=='tone' and a.end==F(1,2) for a in values))
|
|
self.assertTrue(any(len(a.notes)==1 and a.end>=2 for a in values))
|
|
self.assertTrue(any(a.contracts for a in values))
|
|
for a in values:
|
|
checked=b.harmony.evaluate(a.notes,until=a.end,complete=False,expressions=a.contracts)
|
|
self.assertTrue(checked.valid,checked.error)
|
|
evaluation=b.selector.evaluate(a)
|
|
self.assertEqual(evaluation.total,sum(evaluation.contributions.values()))
|
|
self.assertTrue(all(a.start<=n.start<n.end<=a.end for n in a.notes))
|
|
|
|
def test_audible_theme_returns_and_answers_are_reachable(self):
|
|
b=builder(('CM7',),count=2)
|
|
home=(fixtures.note(0,F(1,2),'c'),fixtures.note(F(1,2),1,'d'),fixtures.note(1,2,'e'))
|
|
for kind in ('return','answer'):
|
|
values=b.operators.related(F(4),home,kind)
|
|
self.assertTrue(values,kind)
|
|
self.assertTrue(all(len(a.notes)==len(home) and a.kind==kind for a in values))
|
|
|
|
def test_disabled_expressions_stay_strict(self):
|
|
b=builder(count=1);b.operators.enabled=False
|
|
ps=b.compose();ns=tuple(n for p in ps for n in p.notes)
|
|
self.assertFalse(any(p.expressions for p in ps))
|
|
self.assertTrue(all(n.pitch.number()%12 in types.permitted(b.harmony.choices,n.start,n.end) for n in ns))
|
|
again=b.compose()
|
|
self.assertEqual(ps,again)
|
|
|
|
def test_role_domains_and_complete_tied_position(self):
|
|
for role in ('acoustic','electric'):
|
|
p=profiles.for_part(role);self.assertEqual(p.strings,(1,2,3,4,5,6))
|
|
for midi in range(41,77):self.assertTrue(placement.positions(midi,p))
|
|
a=fixtures.note(0,2,'e',event_id='held');ns=(a,dataclasses.replace(a,start=F(2),end=F(4),continuation=True))
|
|
positioned=placement.place(ns,F(120))[1]
|
|
self.assertEqual(positioned[0].position,positioned[1].position)
|
|
self.assertEqual(len(types.sounding_notes(positioned)),1)
|
|
|
|
def test_complete_routes_cross_boundaries_without_a_forced_chord_tone(self):
|
|
b=builder(('C7','DmM7'),count=2)
|
|
values=b.operators.at(F(3))
|
|
crossed=[a for a in values if a.start<4<a.end and a.kind=='scalar']
|
|
self.assertTrue(crossed)
|
|
for a in crossed:self.assertTrue(b.harmony.evaluate(a.notes).valid)
|
|
one=builder(('C7',),count=1)
|
|
self.assertTrue(any(a.kind=='tone' and a.notes[0].pitch.number()%12==5 and a.end==F(1,2) for a in one.operators.at(F())))
|
|
|
|
def test_actual_incoming_state_changes_ranking(self):
|
|
b=builder(count=1)
|
|
a=b.candidates(F(),construction.State(recent=(fixtures.note(-1,0,'c',octave=3),)))
|
|
c=b.candidates(F(),construction.State(recent=(fixtures.note(-1,0,'c',octave=5),)))
|
|
self.assertNotEqual([v[1].key for v in a],[v[1].key for v in c])
|
|
|
|
def test_handoff_is_not_forced_to_have_a_long_note(self):
|
|
b=builder(count=1)
|
|
values=b.operators.at(F(7,2))
|
|
self.assertTrue(any(a.kind=='tone' and a.end==4 for a in values))
|
|
self.assertTrue(any(a.kind=='rest' and a.end==4 for a in values))
|
|
|
|
def test_cached_actions_do_not_copy_source_ancestry(self):
|
|
b=builder(count=1);a=b.operators.at(F());self.assertIs(a,b.operators.at(F()))
|
|
self.assertTrue(all(not n.sources for action in a for n in action.notes))
|
|
|
|
class EvaluationContract(unittest.TestCase):
|
|
def test_tie_partition_is_invariant_and_reattack_is_distinct(self):
|
|
b=builder(('C7',),count=1);n=fixtures.note(0,4,'e',event_id='hold')
|
|
a=operators.Action(F(),F(4),(n,),'tone')
|
|
split=(dataclasses.replace(n,end=F(2)),dataclasses.replace(n,start=F(2),continuation=True))
|
|
x=b.selector.evaluate(a);y=b.selector.evaluate(dataclasses.replace(a,notes=split))
|
|
self.assertEqual(x,y)
|
|
reattack=tuple(dataclasses.replace(n,continuation=False) for n in split)
|
|
self.assertNotEqual(x,b.selector.evaluate(dataclasses.replace(a,notes=reattack)))
|
|
|
|
def test_target_touch_and_repeated_attacks_do_not_fulfill_hold(self):
|
|
a=fixtures.note(2,4,'e',event_id='arrival')
|
|
anchor=types.Anchor('held',F(2),F(2),a.pitch,frozenset({4}),(41,76),F(2),'terminal','C7',hold_until=F(4))
|
|
ns=(dataclasses.replace(a,end=F(3)),dataclasses.replace(a,start=F(3),event_id='again'))
|
|
self.assertFalse(harmonic.anchors_valid((anchor,),ns,F(4),complete=True,closure='closed')[0])
|
|
self.assertTrue(harmonic.anchors_valid((anchor,),(a,),F(4),complete=True,closure='closed')[0])
|
|
|
|
if __name__=='__main__':unittest.main()
|