Construction

This commit is contained in:
khodowany
2026-10-07 23:45:17 -05:00
parent 3a01b4b6bd
commit 841a3f974e
8 changed files with 236 additions and 80 deletions
+63 -5
View File
@@ -65,6 +65,58 @@ class RecurringGrooves(unittest.TestCase):
selected, _, report = generator.comping_rhythm.choose_cycles(groups(source), 'electric', generator.comping_rhythm.melody_contexts(source), 'coverage')
self.assertTrue(all(p['id'] == 'electric.quarters.statement' for p, _, _ in selected))
self.assertEqual(report[0]['cost'], 0)
def test_acoustic_cycle_is_ranked_after_cross_block_assignment(self):
source = tests.solo_fixtures.song(count=4, chords=('C7',))
resources = copy.deepcopy(generator.vocabulary.load())
policy = resources['accompaniment']['instruments']['acoustic']
policy['groove']['cycle_patterns'] = 1
policy['patterns'] = [{'id': name, 'family': 'quarters', 'lengths': ['4', '4'], 'events': [
{'onset': t, 'duration': '1', 'layer': 'acoustic'} for t in times]}
for name, times in (('crossing', ('0', '1/2', '15/2')), ('local', ('0', '2', '6')))]
def evaluated(events, *args):
# Isolated retrieval prefers 'local' (0) over 'crossing' (1).
# The complete stream permits a prepared upper entry in block two.
cost = -3 if events[0].lane == 'upper' else 1 if events[1].start == fractions.Fraction(1, 2) else 0
return [('test_entry', cost)]
with generator.vocabulary.using(resources, 'acoustic_accompaniment'), unittest.mock.patch('generator.comping_rhythm.evaluate', side_effect=evaluated):
selected, _, reports = generator.comping_rhythm.choose_cycles(groups(source), 'acoustic', generator.comping_rhythm.melody_contexts(source), 'complete')
self.assertEqual([p['id'] for p, _, _ in selected], ['crossing', 'crossing'])
self.assertEqual([r.events[0].lane for _, _, r in selected], ['bass', 'upper'])
self.assertEqual(reports[0]['retrieval_cost'], 4)
self.assertEqual(reports[0]['cost'], -4)
self.assertEqual(reports[0]['cost'], sum(r.cost * 2 for _, _, r in selected))
self.assertEqual(reports[0]['cost'], reports[0]['final_cost'])
for _, _, rhythm in selected:
generator.comping_rhythm.validate_events(rhythm.events, 'acoustic')
def test_acoustic_fill_cannot_reassign_neighboring_attacks(self):
source = tests.solo_fixtures.song(count=8, chords=('C7',))
source['staves'][0]['measures'][2]['section_label'] = 'Source boundary'
resources = copy.deepcopy(generator.vocabulary.load())
policy = resources['accompaniment']['instruments']['acoustic']
policy['patterns'] = [{'id': name, 'family': 'quarters', 'lengths': ['4', '4'], 'events': [
{'onset': t, 'duration': '1/2', 'layer': 'acoustic'} for t in ('0', '2', last)]}
for name, last in (('base', '5'), ('unsafe', '15/2'), ('safe', '7'))]
source_groups = groups(source)
melody = generator.comping_rhythm.melody_contexts(source)
context = generator.comping_rhythm.acoustic_context(source_groups, melody)
base = policy['patterns'][0]
selected = [(base, False, generator.comping_rhythm.realize(base, g, 'acoustic', melody, assign=False)) for g in source_groups]
locations = [{'fill': None} for _ in selected]
original = generator.comping_rhythm.realize_acoustic_form([r for _, _, r in selected], context)
def evaluated(events, part, melody, *args):
# Only the first block has this source entrance. The preferred local
# fill pairs with the next block's first attack, so it is ineligible.
eligible = melody[0][0] == 0
cost = (-2 if events[-1].start == fractions.Fraction(15, 2) else -1 if events[-1].start == 7 else 0) if eligible else 0
return [('test_fill', cost)]
# Distinguish source contexts without relying on the pattern or part ID.
contexts = tuple(m if i < 2 else tuple((a + fractions.Fraction(1, 4), b) for a, b in m) for i, m in enumerate(melody))
context = generator.comping_rhythm.acoustic_context(source_groups, contexts)
with generator.vocabulary.using(resources, 'acoustic_accompaniment'), unittest.mock.patch('generator.comping_rhythm.evaluate', side_effect=evaluated):
generator.comping_rhythm.add_fills(source_groups, selected, locations, 'acoustic', contexts, 'fill', context=context)
self.assertEqual(locations[0]['fill']['pattern'], 'base.fill.safe')
final = generator.comping_rhythm.realize_acoustic_form([r for _, _, r in selected], context)
self.assertEqual([r.events for r in original[1:]], [r.events for r in final[1:]])
def test_melody_and_progression_influence_selection_with_fixed_seed(self):
source = tests.solo_fixtures.song(count=16, chords=('C7', 'FM7'))
sparse = copy.deepcopy(source)
@@ -149,8 +201,9 @@ class RecurringGrooves(unittest.TestCase):
after = canonical(changed[6], changed[5]['harmonies'][0], 6, sources=changed)
self.assertEqual(signature(before), signature(after))
self.assertEqual(canonical.rhythm_report[-1]['base_pattern'], canonical.locations[6]['base_pattern'])
def test_overlap_survives_single_voice_merge_and_tablature(self):
source = tests.solo_fixtures.song(count=2, chords=('C7',))
def test_bass_eighth_then_upper_quarter_survives_single_voice_merge(self):
source = tests.solo_fixtures.song(count=2, chords=('Dm7',))
source['staves'][0]['measures'][0]['harmonies'].append({**generator.harmony.parse_symbol('G7'), 'id': 'middle', 'onset': generator.model.rational(2)})
resources = copy.deepcopy(generator.vocabulary.load())
policy = resources['accompaniment']['instruments']['acoustic']
policy['patterns'] = [p for p in policy['patterns'] if p['id'] == 'acoustic.quarters.overlapping']
@@ -165,9 +218,14 @@ class RecurringGrooves(unittest.TestCase):
generator.acoustic.validate_comping(source)
first = source['staves'][0]['measures'][0]
self.assertEqual(len(first['voices']), 1)
overlap = next(n for n in generator.score.measure_notes(first) if generator.model.fraction(n['onset']) == fractions.Fraction(1, 2))
self.assertEqual(len(overlap['pitches']), 4)
self.assertTrue(any(p.get('tie_from') and p['tablature']['string'] >= 5 for p in overlap['pitches']))
notes = [n for n in generator.score.measure_notes(first) if n['pitches']]
self.assertEqual([(generator.model.fraction(n['onset']), generator.model.fraction(n['duration'])) for n in notes],
[(0, fractions.Fraction(1, 2)), (fractions.Fraction(1, 2), 1), (2, 1)])
strings = [{p['tablature']['string'] for p in n['pitches']} for n in notes]
self.assertIn(strings[0], ({5}, {6}))
self.assertEqual(strings[1], {2, 3, 4})
self.assertIn(strings[2], ({2, 3, 4, 5}, {2, 3, 4, 6}))
self.assertFalse(any(p.get('tie_from') or p.get('tie_to') for n in notes for p in n['pitches']))
def test_mid_measure_chord_change_bass_without_forced_barline_attack(self):
source = tests.solo_fixtures.song(count=2, chords=('C7',))
m = source['staves'][0]['measures'][0]