Solo
This commit is contained in:
+2
-2
@@ -116,7 +116,7 @@ class SoloTests(unittest.TestCase):
|
||||
source = song(count=6, chords=('CM7',))
|
||||
result = compose(source)
|
||||
self.assertLessEqual(len({p.plan.motif.id for p in result.phrases}), 3)
|
||||
self.assertEqual(result.phrases[0].plan.motif.id, result.phrases[1].plan.motif.id)
|
||||
self.assertTrue(all(p.draft.source_phrase for p in result.phrases))
|
||||
self.assertFalse(any(d.treatment.startswith('quote') for p in result.phrases for d in p.decisions))
|
||||
original = result.phrases[0].plan
|
||||
explicit = dataclasses.replace(original, treatment='exact_quote', target=None, anchor_options=(), gestures=(), passage_id='', passage_start=None, passage_end=None)
|
||||
@@ -154,7 +154,7 @@ class SoloTests(unittest.TestCase):
|
||||
self.assertEqual(result.phrases[-1].plan.end, fractions.Fraction(43, 2))
|
||||
def test_search_exhaustion_does_not_claim_impossibility(self):
|
||||
source = song()
|
||||
with unittest.mock.patch('generator.solo.realization.draft_pitch_paths', return_value=((), 'no route within the draft budget')), self.assertRaisesRegex(generator.solo.realization.SearchExhausted, 'not proof'):
|
||||
with unittest.mock.patch('generator.solo.themes.phrase_candidates', return_value=()), self.assertRaisesRegex(generator.solo.realization.SearchExhausted, 'not proof'):
|
||||
compose(source)
|
||||
def test_time_aware_placement_does_not_penalize_absolute_frets(self):
|
||||
p = generator.solo.types.Pitch('c', fractions.Fraction(), 4)
|
||||
|
||||
@@ -187,12 +187,13 @@ class DraftCompositionTests(unittest.TestCase):
|
||||
self.assertTrue(generator.solo.placement.place(draft.notes, self.solo.tempo, previous))
|
||||
self.assertTrue(draft.transformations)
|
||||
previous = phrase.state.last
|
||||
def test_home_survives_several_developments_and_phrase_boundary(self):
|
||||
def test_source_family_survives_phrase_variations(self):
|
||||
first, second = self.solo.phrases
|
||||
self.assertEqual(first.state.home.id, second.state.home.id)
|
||||
self.assertTrue(all(t.home == first.state.home.id for p in self.solo.phrases for t in p.draft.transformations))
|
||||
self.assertNotEqual(second.state.development.id, second.state.home.id)
|
||||
self.assertEqual(second.state.selected_anchors, first.state.selected_anchors)
|
||||
for phrase in self.solo.phrases:
|
||||
self.assertTrue(all(t.home == phrase.state.home.id for t in phrase.draft.transformations))
|
||||
self.assertTrue(phrase.state.home.chord_degrees)
|
||||
self.assertTrue(phrase.draft.source_links)
|
||||
def test_groups_cross_barlines_without_reference_timing_reset(self):
|
||||
# Exercise an explicitly spanning construction; do not require a changed
|
||||
# search objective to pick the same old winner merely to test timing.
|
||||
|
||||
+133
-142
@@ -1,154 +1,145 @@
|
||||
"""Construction and permission contracts; listening remains the taste test."""
|
||||
"""Production source variations, strict scale permission and paired interpretation."""
|
||||
import copy
|
||||
import dataclasses
|
||||
from fractions import Fraction as F
|
||||
import pathlib
|
||||
import unittest
|
||||
from fractions import Fraction as F
|
||||
|
||||
from generator.solo import analysis, composer, drafts, harmonic, leads, references, realization, scoring, types
|
||||
import tests.test_solo as solo_fixture
|
||||
from generator.solo import analysis, composer, drafts, harmonic, leads, placement, realization, scoring, types
|
||||
import generator.fretboard
|
||||
import generator.model
|
||||
import tests.test_solo as fixture
|
||||
import tests.test_solo_development as notes_fixture
|
||||
import tests.test_solo_passages as passage_fixture
|
||||
import tests.test_solo_development as fixture
|
||||
import tests.rhythm_fixtures
|
||||
import tests.scale_fixtures
|
||||
import tests.rhythm_fixtures
|
||||
import tests.solo_fixtures
|
||||
|
||||
class ChromaticApproachTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.harmony = harmonic.Harmony(passage_fixture.choices(('C7', 'FM7')))
|
||||
self.approach = dataclasses.replace(fixture.note(F(7, 2), 4, 'g', -1), event_id='approach')
|
||||
self.target = dataclasses.replace(fixture.note(4, 6, 'f'), event_id='resolution')
|
||||
self.treatment = types.HarmonicTreatment('test', 'chromatic_approach', 66, F(7, 2), F(4), 65, F(4), F(), 'approach', 'resolution')
|
||||
def test_approach_resolves_across_chord_boundary_without_remapping_scale(self):
|
||||
self.assertFalse(self.harmony.evaluate((self.approach, self.target), complete=True).valid)
|
||||
result = self.harmony.evaluate((self.approach, self.target), (self.treatment,), complete=True)
|
||||
self.assertTrue(result.valid, result.error)
|
||||
self.assertFalse(result.pending)
|
||||
report = self.harmony.describe((self.approach, self.target), (self.treatment,))
|
||||
self.assertEqual((report[0]['function'], report[0]['motion']), ('outside_collection', 'chromatic_approach'))
|
||||
def test_pending_approach_survives_computational_boundary(self):
|
||||
partial = self.harmony.evaluate((self.approach,), (self.treatment,), until=F(4))
|
||||
self.assertTrue(partial.valid)
|
||||
self.assertEqual(partial.pending, (self.treatment,))
|
||||
self.assertFalse(self.harmony.evaluate((self.approach,), (self.treatment,), until=F(5)).valid)
|
||||
def test_tie_partition_does_not_change_approach_or_sustain(self):
|
||||
split = (dataclasses.replace(self.approach, end=F(15, 4)), dataclasses.replace(self.approach, start=F(15, 4), continuation=True, event_id='tie'), dataclasses.replace(self.target, end=F(5)), dataclasses.replace(self.target, start=F(5), continuation=True, event_id='target_tie'))
|
||||
whole = (self.approach, self.target)
|
||||
self.assertEqual(self.harmony.evaluate(whole, (self.treatment,), complete=True), self.harmony.evaluate(split, (self.treatment,), complete=True))
|
||||
self.assertEqual(self.harmony.describe(whole, (self.treatment,)), self.harmony.describe(split, (self.treatment,)))
|
||||
self.assertEqual(len(types.sounding_notes(split)), 2)
|
||||
def test_labels_cannot_excuse_arbitrary_notes(self):
|
||||
cases = [
|
||||
(dataclasses.replace(self.approach, start=F(3)), self.target, self.treatment),
|
||||
(self.approach, dataclasses.replace(self.target, start=F(9, 2)), self.treatment),
|
||||
(self.approach, dataclasses.replace(self.target, pitch=fixture.note(0, 1, 'e').pitch), self.treatment),
|
||||
(self.approach, self.target, dataclasses.replace(self.treatment, event='unrelated')),
|
||||
(dataclasses.replace(self.approach, role='quote'), self.target, self.treatment),
|
||||
(self.approach, self.target, dataclasses.replace(self.treatment, deadline=F(5))),
|
||||
]
|
||||
for approach, target, treatment in cases:
|
||||
with self.subTest(approach=approach, target=target, treatment=treatment):
|
||||
self.assertFalse(self.harmony.evaluate((approach, target), (treatment,), complete=True).valid)
|
||||
def test_resolution_must_be_chord_member_and_valid_through_its_hold(self):
|
||||
nonmember = dataclasses.replace(self.target, pitch=fixture.note(4, 6, 'g').pitch)
|
||||
treatment = dataclasses.replace(self.treatment, resolution=67)
|
||||
self.assertFalse(self.harmony.evaluate((self.approach, nonmember), (treatment,), complete=True).valid)
|
||||
long_approach = dataclasses.replace(self.approach, start=F(2))
|
||||
self.assertFalse(self.harmony.evaluate((long_approach, self.target), (dataclasses.replace(self.treatment, start=F(2)),), complete=True).valid)
|
||||
def test_approach_spelling_avoids_unnecessary_double_accidentals(self):
|
||||
target = fixture.note(0, 1, 'a', -1).pitch
|
||||
self.assertEqual(leads.approach_pitch(target, 69).label(), 'A4')
|
||||
target = fixture.note(0, 1, 'e').pitch
|
||||
self.assertEqual(leads.approach_pitch(target, 63).label(), 'D#4')
|
||||
def test_chromatic_spacing_carries_across_passage_boundary(self):
|
||||
notes = (dataclasses.replace(self.approach, pitch=fixture.note(0, 1, 'g').pitch), self.target)
|
||||
plan = dataclasses.replace(fixture.plan(), end=F(8), release=F(2), target=None, passages=())
|
||||
group = types.Transformation('group', '', '', 'answer', F(), F(8), tuple(n.event_id for n in notes), tuple((n.start, n.end) for n in notes), preserves=())
|
||||
draft = types.MelodicDraft('spacing', F(), F(8), notes, (group,), ('resolution',), (), (), None, 'answer')
|
||||
empty = types.ContinuationState('previous')
|
||||
phrase = realization.checked_phrase(plan, draft, notes, (), empty, self.harmony, F(120))
|
||||
self.assertIsNotNone(phrase)
|
||||
self.assertTrue(leads.chromatic(phrase, empty, self.harmony, F(120)).edits)
|
||||
recent = dataclasses.replace(empty, treatments=(dataclasses.replace(self.treatment, start=F(-7, 2)),))
|
||||
self.assertFalse(leads.chromatic(phrase, recent, self.harmony, F(120)).edits)
|
||||
|
||||
class CompleteLeadTests(unittest.TestCase):
|
||||
class SourceVariationTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.references = references.load_library(pathlib.Path('data/lead_phrases'))
|
||||
cls.source = solo_fixture.song(count=8, chords=('Gm7', 'C7', 'FM7', 'BbM7', 'Em7b5', 'A7', 'Dm7', 'Dm7'))
|
||||
cls.context = realization.SearchContext()
|
||||
cls.result = cls.compose(cls.context)
|
||||
cls.document=fixture.song(count=8,chords=('CM7',))
|
||||
cls.context=realization.SearchContext()
|
||||
cls.solo=cls.compose(cls.document,cls.context)
|
||||
@classmethod
|
||||
def compose(cls, context, stage='elaborated'):
|
||||
return composer.compose(analysis.analyze(cls.source), cls.source['staves'][0]['measures'], 'acoustic', F(32), scales=tests.scale_fixtures.library(), sequences=tests.solo_fixtures.library(), rhythms=tests.rhythm_fixtures.library(), references=cls.references, context=context, stage=stage)
|
||||
def test_complete_tune_precedes_decorations_and_contains_a_return(self):
|
||||
for phrase in self.result.phrases:
|
||||
self.assertTrue(phrase.draft.lead_identity)
|
||||
self.assertTrue(phrase.draft.correspondence)
|
||||
self.assertTrue(drafts.contract_valid(phrase.initial, phrase.initial.notes)[0])
|
||||
self.assertTrue(drafts.contract_valid(phrase.initial, realization.emitted_notes(phrase))[0])
|
||||
source = {n.event_id: n for n in phrase.initial.notes}
|
||||
for a, b in phrase.draft.correspondence:
|
||||
self.assertEqual(source[a].end - source[a].start, source[b].end - source[b].start)
|
||||
self.assertNotIn('diminish', [t.operation for t in phrase.draft.transformations])
|
||||
for route in phrase.routes:
|
||||
run = peak = 0
|
||||
end = None
|
||||
for note in route.notes:
|
||||
eighth = note.end-note.start == F(1,2)
|
||||
run = run+1 if eighth and end == note.start else int(eighth)
|
||||
peak = max(peak, run)
|
||||
end = note.end
|
||||
self.assertGreaterEqual(peak, 4)
|
||||
composer.validate(self.result)
|
||||
self.assertTrue(all(1 <= n.position.fret <= 12 for n in self.result.notes()))
|
||||
def test_same_inputs_and_cache_preserve_music(self):
|
||||
second = self.compose(self.context)
|
||||
self.assertEqual(self.result.notes(), second.notes())
|
||||
self.assertGreater(self.context.diagnostics['cache_hits'], 0)
|
||||
def test_lead_evaluation_is_independent_of_computational_partitions(self):
|
||||
phrase = self.result.phrases[0]
|
||||
whole = phrase.initial.notes
|
||||
split = []
|
||||
for note in whole:
|
||||
cuts = sorted({note.start,note.end} | {F(i,2) for i in range(64) if note.start < F(i,2) < note.end})
|
||||
split.extend(dataclasses.replace(note,start=a,end=b,continuation=i>0) for i,(a,b) in enumerate(zip(cuts,cuts[1:])))
|
||||
harmony = harmonic.Harmony(self.result.choices)
|
||||
self.assertEqual(harmony.describe(whole),harmony.describe(split))
|
||||
self.assertEqual(scoring.features(phrase.plan,whole,self.result.choices),scoring.features(phrase.plan,split,self.result.choices))
|
||||
self.assertTrue(drafts.contract_valid(phrase.initial,split)[0])
|
||||
def test_activity_and_occupancy_are_ranges(self):
|
||||
plan = dataclasses.replace(fixture.plan(), start=F(), end=F(4), release=F(), treatment='lead', passages=())
|
||||
choices = passage_fixture.choices(('C7',))
|
||||
for count in (4, 5, 6):
|
||||
notes = tuple(fixture.note(F(4*i, count), F(4*(i+1), count), 'e') for i in range(count))
|
||||
values = scoring.features(plan, notes, choices)
|
||||
self.assertEqual(values['activity'], 0)
|
||||
self.assertEqual(values['occupancy'], 0)
|
||||
self.assertFalse(scoring.feasible(plan, (), choices, F(4)))
|
||||
def test_new_identity_is_a_joint_rhythm_and_pitch_object(self):
|
||||
identities = leads.identities(self.references, types.ContinuationState(''))
|
||||
self.assertEqual(len(identities), 12)
|
||||
self.assertGreater(len({i.durations for i in identities}), 1)
|
||||
self.assertGreater(len({i.degrees for i in identities}), 1)
|
||||
for identity in identities:
|
||||
self.assertEqual(len(identity.degrees), len(identity.durations))
|
||||
self.assertEqual(sum(identity.durations), 4)
|
||||
def test_invented_material_can_be_replaced_but_committed_hook_is_preserved(self):
|
||||
phrase = self.result.phrases[0]
|
||||
changed = tuple(n for n in phrase.initial.notes if n.event_id != phrase.draft.correspondence[0][1])
|
||||
self.assertFalse(drafts.contract_valid(phrase.initial, changed)[0])
|
||||
def test_missing_harmony_remains_space_with_lead_generation_enabled(self):
|
||||
document = solo_fixture.song(count=4, chords=())
|
||||
solo = composer.compose(analysis.analyze(document), document['staves'][0]['measures'], 'acoustic', F(16), scales=tests.scale_fixtures.library(), sequences=tests.solo_fixtures.library(), rhythms=tests.rhythm_fixtures.library(), references=self.references)
|
||||
self.assertFalse(solo.notes())
|
||||
composer.validate(solo)
|
||||
def test_vocal_pickup_cutoff_does_not_compress_a_full_ending(self):
|
||||
solo = composer.compose(analysis.analyze(self.source), self.source['staves'][0]['measures'], 'electric', F(29), scales=tests.scale_fixtures.library(), sequences=tests.solo_fixtures.library(), rhythms=tests.rhythm_fixtures.library(), references=self.references)
|
||||
composer.validate(solo)
|
||||
self.assertTrue(all(n.end <= 29 and n.end-n.start >= F(1,2) for n in solo.notes()))
|
||||
tail = [n for n in solo.notes() if n.start >= 28]
|
||||
self.assertEqual([(n.start,n.end) for n in tail], [(28,29)])
|
||||
def compose(cls,document,context=None,end=32):
|
||||
return composer.compose(analysis.analyze(document),document['staves'][0]['measures'],'acoustic',F(end),
|
||||
scales=tests.scale_fixtures.library(),sequences=tests.solo_fixtures.library(),rhythms=tests.rhythm_fixtures.library(),context=context,mode='paraphrase')
|
||||
def test_production_is_traceable_to_complete_source_phrases(self):
|
||||
self.assertTrue(self.solo.phrases)
|
||||
for p in self.solo.phrases:
|
||||
self.assertIsNotNone(p.draft.source_phrase)
|
||||
self.assertTrue(p.draft.source_links)
|
||||
self.assertTrue(p.edits)
|
||||
self.assertGreater(len(p.draft.source_phrase.notes),8)
|
||||
self.assertTrue(drafts.contract_valid(p.draft,composer.phrase_notes(p))[0])
|
||||
self.assertTrue(all(n.sources for n in p.draft.notes))
|
||||
def test_same_input_and_cache_preserve_complete_music(self):
|
||||
other=self.compose(self.document,self.context)
|
||||
self.assertEqual(self.solo.notes(),other.notes())
|
||||
self.assertGreater(self.context.diagnostics['cache_hits'],0)
|
||||
def test_source_change_drives_music_with_fixed_harmony(self):
|
||||
changed=copy.deepcopy(self.document)
|
||||
for m in changed['staves'][0]['measures']:
|
||||
for n in m['voices'][0]['notes']:
|
||||
n['pitches'][0]['letter']='g' if n['pitches'][0]['letter']=='c' else n['pitches'][0]['letter']
|
||||
other=self.compose(changed)
|
||||
self.assertNotEqual(leads.signature(self.solo.notes()),leads.signature(other.notes()))
|
||||
self.assertNotEqual(self.solo.phrases[0].initial.notes[0].pitch.number()%12,other.phrases[0].initial.notes[0].pitch.number()%12)
|
||||
def test_source_ties_and_rests_are_retained_before_variation(self):
|
||||
melody=(notes_fixture.note(0,5,'c'),notes_fixture.note(6,9,'e'),notes_fixture.note(9,12,'g'),notes_fixture.note(12,16,'c'))
|
||||
melody=tuple(dataclasses.replace(n,sources=(f's{i}',)) for i,n in enumerate(melody))
|
||||
source=analysis.source_phrases(melody,F(),F(16),F(4))
|
||||
self.assertEqual(tuple(n for p in source for n in p.notes)[0].end,5)
|
||||
self.assertTrue(any((F(5),F(6)) in p.rests for p in source))
|
||||
def test_strict_map_rejects_chromatic_labels(self):
|
||||
harmony=harmonic.Harmony(passage_fixture.choices(('C7',)))
|
||||
approach=dataclasses.replace(notes_fixture.note(F(5,2),3,'f',1),event_id='approach')
|
||||
target=dataclasses.replace(notes_fixture.note(3,4,'g'),event_id='target')
|
||||
contract=types.HarmonicTreatment('removed','chromatic_approach',66,F(5,2),F(3),67,F(3),F(),'approach','target')
|
||||
self.assertFalse(harmony.evaluate((approach,target),complete=True).valid)
|
||||
self.assertFalse(harmony.evaluate((approach,target),(contract,),complete=True).valid)
|
||||
def test_mapped_accidentals_and_half_steps_remain_valid(self):
|
||||
scale=tests.scale_fixtures.library().scale('harmonic_minor',1,'D')
|
||||
choices=passage_fixture.choices(('A7','Dm7'),duration=2,scale=scale)
|
||||
harmony=harmonic.Harmony(choices)
|
||||
notes=(notes_fixture.note(0,F(3,2),'e'),notes_fixture.note(F(3,2),2,'c',1),notes_fixture.note(2,4,'d'))
|
||||
self.assertTrue(harmony.evaluate(notes,complete=True).valid)
|
||||
def test_partition_does_not_change_sounding_evaluation(self):
|
||||
phrase=self.solo.phrases[0]
|
||||
whole=phrase.draft.notes;split=[]
|
||||
for n in whole:
|
||||
middle=(n.start+n.end)/2
|
||||
split.extend((dataclasses.replace(n,end=middle),dataclasses.replace(n,start=middle,continuation=True)))
|
||||
harmony=harmonic.Harmony(self.solo.choices)
|
||||
self.assertEqual(harmony.evaluate(whole,complete=True),harmony.evaluate(split,complete=True))
|
||||
self.assertEqual(scoring.features(phrase.plan,whole,self.solo.choices),scoring.features(phrase.plan,split,self.solo.choices))
|
||||
self.assertTrue(drafts.contract_valid(phrase.draft,split)[0])
|
||||
def test_low_register_and_tied_placement(self):
|
||||
low=dataclasses.replace(notes_fixture.note(0,2,'a'),pitch=types.Pitch('a',F(),2))
|
||||
following=dataclasses.replace(low,start=2,end=4,continuation=True)
|
||||
placed=placement.place((low,following),F(120))
|
||||
self.assertEqual(placed[1][0].position.string_number,6)
|
||||
self.assertEqual(placed[1][0].position,placed[1][1].position)
|
||||
self.assertEqual({p.string_number for number in range(41,77) for p in placement.positions(number)},set(range(1,7)))
|
||||
def test_pair_metric_detects_octave_copy(self):
|
||||
shifted=dataclasses.replace(self.solo,phrases=tuple(dataclasses.replace(p,decisions=tuple(dataclasses.replace(d,notes=leads.shift(d.notes,1)) for d in p.decisions)) for p in self.solo.phrases))
|
||||
self.assertEqual(leads.similarity(self.solo,shifted),1)
|
||||
callback=dataclasses.replace(shifted,phrases=tuple(dataclasses.replace(p,plan=dataclasses.replace(p.plan,relationship='callback')) for p in shifted.phrases))
|
||||
self.assertEqual(leads.similarity(self.solo,callback),0)
|
||||
def test_later_arrivals_compare_complete_routes_with_different_holds(self):
|
||||
notes=tuple(dataclasses.replace(notes_fixture.note(a,b,k),event_id=str(a),sources=(str(a),))
|
||||
for a,b,k in ((0,1,'g'),(1,2,'e'),(2,8,'c')))
|
||||
source=types.SourcePhrase('ending',F(),F(8),notes,(),'family')
|
||||
harmony=harmonic.Harmony(passage_fixture.choices(('C7','C7')))
|
||||
options=leads.elaborate(notes,(),harmony,tests.solo_fixtures.library(),tests.rhythm_fixtures.library(),'4/4',source)
|
||||
self.assertEqual({r.destination.end-r.destination.start for _,_,r in options},{F(1,2),F(1),F(3,2),F(2)})
|
||||
for actual,_,route in options:
|
||||
self.assertTrue(harmony.evaluate(actual,complete=True).valid)
|
||||
self.assertEqual(actual[-1],route.destination)
|
||||
self.assertEqual((actual[0].start,actual[-1].end),(0,8))
|
||||
def test_repeated_source_cadence_is_not_accidental_monotony(self):
|
||||
first=self.solo.phrases[0]
|
||||
changed=dataclasses.replace(first,draft=dataclasses.replace(first.draft,
|
||||
source_phrase=dataclasses.replace(first.draft.source_phrase,family='unrelated')))
|
||||
self.assertAlmostEqual(leads.transition(first,changed)-leads.transition(first,first),.15)
|
||||
def test_space_and_vocal_return_are_preserved(self):
|
||||
silent=self.compose(fixture.song(count=8,chords=()))
|
||||
self.assertFalse(silent.notes())
|
||||
shortened=self.compose(self.document,end=F(29))
|
||||
self.assertTrue(all(n.end<=29 for n in shortened.notes()))
|
||||
composer.validate(shortened)
|
||||
def test_pair_selection_produces_valid_distinct_source_variations(self):
|
||||
a=analysis.analyze(self.document)
|
||||
pair=composer.compose_pair(a,{part:(self.document['staves'][0]['measures'],F(32),None) for part in ('acoustic','electric')},
|
||||
scales=tests.scale_fixtures.library(),sequences=tests.solo_fixtures.library(),rhythms=tests.rhythm_fixtures.library(),mode='paraphrase')
|
||||
self.assertLess(leads.similarity(*pair),1)
|
||||
self.assertEqual(pair[0].diagnostics['pairs_compared'],64)
|
||||
for solo in pair:
|
||||
composer.validate(solo)
|
||||
for phrase in solo.phrases:
|
||||
heard={n.event_id:n for n in composer.phrase_notes(phrase)}
|
||||
for route in phrase.routes:
|
||||
self.assertEqual(route.notes[-1].pitch,route.destination.pitch)
|
||||
for note in route.notes:
|
||||
self.assertEqual((heard[note.event_id].pitch,heard[note.event_id].start,heard[note.event_id].end),(note.pitch,note.start,note.end))
|
||||
def test_pickup_is_brought_inside_window_without_touching_previous_section(self):
|
||||
melody=(notes_fixture.note(0,5,'g'),notes_fixture.note(5,6,'a'),notes_fixture.note(6,8,'g'))
|
||||
melody=tuple(dataclasses.replace(n,sources=(f's{i}',),event_id=f's{i}') for i,n in enumerate(melody))
|
||||
pickup=tuple(dataclasses.replace(notes_fixture.note(i-3,i-2,name),sources=(f'p{i}',),event_id=f'p{i}') for i,name in enumerate(('c','d','e')))
|
||||
source=types.SourcePhrase('pickup',F(),F(8),melody,(),'family',pickup)
|
||||
harmony=harmonic.Harmony(passage_fixture.choices(('C7','C7')))
|
||||
options=[notes for notes,edits in leads.variations(melody,source,harmony) if edits[0].kind=='pickup_entrance']
|
||||
self.assertTrue(options)
|
||||
self.assertEqual(options[0][0].start,0)
|
||||
self.assertEqual(options[0][3].start,3)
|
||||
self.assertEqual(options[0][3].end,5)
|
||||
self.assertTrue(harmony.evaluate(options[0],complete=True).valid)
|
||||
def test_occupancy_is_not_an_objective_and_active_silence_is_invalid(self):
|
||||
plan=dataclasses.replace(notes_fixture.plan(),end=F(4),release=F(),passages=(),treatment='source_variation')
|
||||
choices=passage_fixture.choices(('C7',))
|
||||
for end in (F(1),F(4)):
|
||||
self.assertEqual(scoring.features(plan,(notes_fixture.note(0,end,'e'),),choices)['occupancy'],0)
|
||||
self.assertFalse(scoring.feasible(plan,(),choices,F(4)))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
if __name__=='__main__':unittest.main()
|
||||
|
||||
@@ -115,24 +115,15 @@ class HarmonicPassageTests(unittest.TestCase):
|
||||
self.assertEqual(generator.solo.types.scale_index(scale, 65) - generator.solo.types.scale_index(scale, 64), 1)
|
||||
|
||||
class PassageStateTests(unittest.TestCase):
|
||||
def test_eight_bar_cadence_carries_realized_identity_and_anchor_line(self):
|
||||
def test_source_phrases_carry_identity_and_completed_anchors(self):
|
||||
import tests.test_solo
|
||||
solo = tests.test_solo.compose(tests.test_solo.song(count=8, chords=('Gm7', 'C7', 'FM7', 'BbM7', 'Em7b5', 'A7', 'Dm7', 'Dm7')))
|
||||
self.assertEqual(len(solo.passages), 1)
|
||||
self.assertEqual((solo.passages[0].start, solo.passages[0].end), (0, 32))
|
||||
self.assertEqual([(p.plan.start, p.plan.end) for p in solo.phrases], [(0, 16), (16, 32)])
|
||||
first, second = solo.phrases
|
||||
self.assertEqual(first.plan.ending, 'connected')
|
||||
self.assertEqual(first.state.motifs, second.state.motifs[:len(first.state.motifs)])
|
||||
self.assertEqual(first.anchors, second.anchors)
|
||||
self.assertTrue(any(a.role == 'intermediate' for a in second.anchors))
|
||||
self.assertTrue(all(generator.solo.harmonic.anchor_note(a, second.state.notes) for a in second.anchors))
|
||||
heard = {m.id: m for m in second.state.motifs}
|
||||
self.assertGreaterEqual(len(heard), 5)
|
||||
for motif in list(heard.values())[1:]:
|
||||
self.assertIn(motif.parent, heard)
|
||||
self.assertTrue(motif.chord_degrees)
|
||||
self.assertTrue(generator.solo.harmonic.anchors_valid(second.anchors, solo.notes(), solo.end, complete=True, closure='closed')[0])
|
||||
self.assertEqual((solo.passages[0].start, solo.passages[-1].end), (0, 32))
|
||||
for phrase in solo.phrases:
|
||||
self.assertTrue(phrase.draft.source_phrase)
|
||||
self.assertTrue(phrase.state.home.chord_degrees)
|
||||
self.assertTrue(all(generator.solo.harmonic.anchor_note(a, phrase.state.notes) for a in phrase.anchors))
|
||||
self.assertTrue(generator.solo.harmonic.anchors_valid(phrase.anchors, phrase.state.notes, phrase.plan.end, complete=True, closure='closed')[0])
|
||||
def test_development_references_realization_instead_of_source_template(self):
|
||||
plan = tests.test_solo_development.plan()
|
||||
harmony = generator.solo.harmonic.Harmony(choices(('C7',)))
|
||||
|
||||
@@ -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