Construction
This commit is contained in:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user