Solo
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""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.assertEqual(self.solo.phrases[0].plan.closure,'open')
|
||||
self.assertTrue(all(a.role=='intermediate' for a in self.solo.phrases[0].anchors))
|
||||
self.assertEqual(self.solo.phrases[-1].anchors[-1].role,'terminal')
|
||||
self.assertTrue(any(t.operation=='repeat_with_harmonic_adaptation' for p in self.solo.phrases for t in p.draft.transformations))
|
||||
|
||||
def test_library_routes_match_the_actual_complete_theme(self):
|
||||
self.assertTrue(any(p.routes for p in self.solo.phrases))
|
||||
for phrase in 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_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()
|
||||
Reference in New Issue
Block a user