148 lines
8.6 KiB
Python
148 lines
8.6 KiB
Python
"""Source influence and realized-theme continuity are different contracts."""
|
|
import dataclasses
|
|
from fractions import Fraction as F
|
|
import unittest
|
|
|
|
from generator.solo import analysis, composer, drafts, harmonic, leads, references, realization, similarity, themes, types
|
|
import tests.test_solo as fixture
|
|
import tests.test_solo_development as notes_fixture
|
|
import tests.test_solo_passages as harmony_fixture
|
|
import tests.scale_fixtures
|
|
import tests.solo_fixtures
|
|
import tests.rhythm_fixtures
|
|
|
|
|
|
def melody(values):
|
|
return tuple(dataclasses.replace(notes_fixture.note(a,b,p),event_id=str(i),sources=(str(i),)) for i,(a,b,p) in enumerate(values))
|
|
|
|
class ThemeContracts(unittest.TestCase):
|
|
def test_selected_rhythm_does_not_also_require_source_pitches(self):
|
|
source=melody(((0,1,'c'),(1,2,'d'),(2,3,'e')))
|
|
ref=types.SourceReference('source','phrase',F(),F(3),source,'rhythmic_seed')
|
|
actual=melody(((8,9,'g'),(9,10,'e'),(10,11,'c')))
|
|
self.assertEqual(references.feature_error(themes.feature(ref,actual),actual),0)
|
|
contour=dataclasses.replace(ref,treatment='contour_seed')
|
|
self.assertGreater(references.feature_error(themes.feature(contour,actual),actual),0)
|
|
|
|
def test_a_single_event_is_not_evidence_of_a_multinote_feature(self):
|
|
source=melody(((0,1,'c'),(1,2,'d'),(2,3,'e')))
|
|
ref=types.SourceReference('source','phrase',F(),F(3),source,'rhythmic_seed')
|
|
contract=themes.feature(ref,source)
|
|
self.assertEqual(references.feature_error(contract,source[:1]),1)
|
|
self.assertEqual(references.feature_error(themes.feature(ref,source[:1]),source[:1]),1)
|
|
|
|
def test_copy_comparison_cannot_be_diluted_by_octaves_runs_or_reattacks(self):
|
|
source=melody(((0,2,'c'),(2,4,'e'),(4,6,'g'),(6,8,'c')))
|
|
shifted=leads.shift(source,1)
|
|
self.assertTrue(similarity.near_copy(shifted,source,F(),F(8))['near_copy'])
|
|
repeated=tuple(part for n in shifted for part in (dataclasses.replace(n,end=n.start+1),dataclasses.replace(n,start=n.start+1,event_id=n.event_id+'r')))
|
|
self.assertTrue(similarity.near_copy(repeated,source,F(),F(8))['near_copy'])
|
|
self.assertEqual(similarity.compare(repeated,source)['octave_insensitive'],1)
|
|
shortened=tuple(dataclasses.replace(n,end=n.end-F(1,2)) for n in shifted)
|
|
self.assertEqual(similarity.compare(shortened,source)['octave_insensitive'],.75)
|
|
relocated=tuple(dataclasses.replace(n,start=n.start+8,end=n.end+8) for n in shifted)
|
|
self.assertTrue(similarity.near_copy(relocated,source,F(8),F(16))['near_copy'])
|
|
|
|
def test_tied_partition_does_not_change_similarity(self):
|
|
source=melody(((0,2,'c'),(2,4,'e'),(4,6,'g'),(6,8,'c')))
|
|
split=tuple(piece for n in source for piece in (dataclasses.replace(n,end=n.start+1),dataclasses.replace(n,start=n.start+1,continuation=True)))
|
|
self.assertEqual(similarity.compare(source,source),similarity.compare(split,source))
|
|
|
|
def test_downward_cross_collection_steps_preserve_the_actual_target(self):
|
|
from generator.solo import routes
|
|
scale=tests.scale_fixtures.library().scale('major',6,'D')
|
|
harmony=harmonic.Harmony(harmony_fixture.choices(('Dm7',),scale=scale))
|
|
first=dataclasses.replace(notes_fixture.note(0,1,'c'),pitch=types.Pitch('c',F(1),4))
|
|
last=dataclasses.replace(notes_fixture.note(1,2,'b'),pitch=types.Pitch('b',F(-1),3))
|
|
step,=themes.relative_steps((first,last),harmony)
|
|
self.assertEqual(step,-2)
|
|
self.assertEqual(routes.advance(scale,first.pitch.number(),step)[0].number(),last.pitch.number())
|
|
|
|
def test_two_note_route_uses_exact_scale_displacement(self):
|
|
from generator.solo import routes
|
|
harmony=harmonic.Harmony(harmony_fixture.choices(('C7',)))
|
|
notes=melody(((0,1,'g'),(1,2,'e')))
|
|
options=routes.candidates(notes,{'1'},harmony,tests.solo_fixtures.library(),(41,76))
|
|
self.assertTrue(options)
|
|
self.assertTrue(all(r.notes[-1].pitch==notes[-1].pitch for r in options))
|
|
|
|
def test_elaboration_may_continue_its_entry_instead_of_reattacking(self):
|
|
notes=melody(((0,3,'g'),(3,4,'e')))
|
|
source=types.SourcePhrase('held_entry',F(),F(4),notes,(),'')
|
|
harmony=harmonic.Harmony(harmony_fixture.choices(('C7',)))
|
|
options=leads.elaborate(notes,(),harmony,tests.solo_fixtures.library(),tests.rhythm_fixtures.library(),'4/4',source,sustain_entry=True)
|
|
self.assertTrue(options)
|
|
for actual,edit,route in options:
|
|
self.assertEqual(actual[0].start,0)
|
|
self.assertGreater(actual[0].end,edit.start)
|
|
self.assertEqual(actual[:len(route.notes)],route.notes)
|
|
self.assertTrue(harmony.evaluate(actual,complete=True).valid)
|
|
|
|
class ThemeProduction(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.document=fixture.song(count=8,chords=('CM7',))
|
|
cls.analysis=analysis.analyze(cls.document)
|
|
cls.context=realization.SearchContext()
|
|
cls.kw=dict(scales=tests.scale_fixtures.library(),sequences=tests.solo_fixtures.library(),rhythms=tests.rhythm_fixtures.library(),context=cls.context)
|
|
cls.solo=composer.compose(cls.analysis,cls.document['staves'][0]['measures'],'acoustic',F(32),**cls.kw)
|
|
|
|
def test_one_heard_theme_develops_across_source_boundaries(self):
|
|
self.assertEqual(self.solo.diagnostics['construction'],'thematic_recomposition')
|
|
self.assertEqual(len({p.state.home.id for p in self.solo.phrases}),1)
|
|
for phrase in self.solo.phrases:
|
|
self.assertTrue(drafts.contract_valid(phrase.draft,composer.phrase_notes(phrase))[0])
|
|
self.assertFalse(similarity.near_copy(phrase.refined.notes,themes.source_notes(self.analysis),phrase.plan.start,phrase.plan.end)['near_copy'])
|
|
self.assertIn(self.solo.phrases[0].plan.closure,('open','closed'))
|
|
self.assertTrue(all(p.evaluation and p.score==p.evaluation.total for p in self.solo.phrases))
|
|
self.assertEqual(self.solo.phrases[-1].anchors[-1].role,'terminal')
|
|
self.assertTrue(any(t.operation in ('develop_contour','develop_rhythm','continue','return') for p in self.solo.phrases for t in p.draft.transformations))
|
|
|
|
def test_library_routes_match_the_actual_complete_theme(self):
|
|
plain=composer.compose(self.analysis,self.document['staves'][0]['measures'],'acoustic',F(32),stage='plain',**self.kw)
|
|
# Joint rhythm/expression edits can replace every original route in this
|
|
# particular fixture. Require construction to use complete routes, then
|
|
# verify all retained provenance in both the plain and edited realizations.
|
|
self.assertTrue(any(p.routes for p in plain.phrases))
|
|
for phrase in (*plain.phrases,*self.solo.phrases):
|
|
heard={n.event_id:n for n in composer.phrase_notes(phrase)}
|
|
for route in phrase.routes:
|
|
for note in route.notes:
|
|
actual=heard[note.event_id]
|
|
self.assertEqual((note.start,note.end,note.pitch),(actual.start,actual.end,actual.pitch))
|
|
self.assertEqual(route.notes[-1].pitch,route.destination.pitch)
|
|
|
|
def test_report_can_compare_a_source_hold_cut_by_new_phrase_boundaries(self):
|
|
from generator.solo import report
|
|
phrases=[]
|
|
for i,p in enumerate(self.solo.phrases):
|
|
held=notes_fixture.note(p.plan.start,p.plan.end,'c',event_id='source_hold',continuation=i>0)
|
|
phrases.append(dataclasses.replace(p,initial=dataclasses.replace(p.initial,notes=(held,))))
|
|
solo=dataclasses.replace(self.solo,phrases=tuple(phrases))
|
|
detail=report.structure(solo)
|
|
self.assertEqual(len(detail['phrases']),len(phrases))
|
|
self.assertTrue(phrases[-1].initial.notes[0].continuation)
|
|
|
|
def test_incoming_pitch_changes_concrete_continuation_pool(self):
|
|
harmony=harmonic.Harmony(self.solo.choices)
|
|
home=self.solo.phrases[0].state.home
|
|
ref=themes.home_reference(home,'answer')
|
|
context=realization.SearchContext()
|
|
outputs=[]
|
|
for octave in (3,4):
|
|
prior=dataclasses.replace(notes_fixture.note(15,16,'e'),pitch=types.Pitch('e',F(),octave))
|
|
state=types.ContinuationState('prior',last=prior,home=home)
|
|
options=themes.unit_candidates(ref,F(16),F(24),state,harmony,self.kw['sequences'],self.kw['rhythms'],'4/4',context,home=home,operation='answer')
|
|
self.assertTrue(options)
|
|
outputs.append({u.notes[0].pitch.number() for u in options})
|
|
self.assertNotEqual(*outputs)
|
|
|
|
def test_deterministic_cache_and_actual_source_input(self):
|
|
repeated=composer.compose(self.analysis,self.document['staves'][0]['measures'],'acoustic',F(32),**self.kw)
|
|
self.assertEqual(self.solo.notes(),repeated.notes())
|
|
changed=dataclasses.replace(self.analysis,melody=tuple(dataclasses.replace(n,pitch=dataclasses.replace(n.pitch,letter='g')) for n in self.analysis.melody))
|
|
other=composer.compose(changed,self.document['staves'][0]['measures'],'acoustic',F(32),**self.kw)
|
|
self.assertNotEqual(leads.signature(self.solo.notes()),leads.signature(other.notes()))
|
|
|
|
if __name__=='__main__':unittest.main()
|