Generation
This commit is contained in:
+53
-36
@@ -17,10 +17,9 @@ def grip(frets, strings=(4, 3, 2, 1)):
|
||||
|
||||
def ranked(symbol, melody=(), profile='electric', prune=False):
|
||||
definition = generator.harmony.parse_symbol(symbol)
|
||||
required, allowed, bass = generator.guitar.chord_requirements(definition, profile)
|
||||
root = int((generator.model.NATURAL[definition['root']['letter']] + generator.model.alteration(definition['root']['alter'])) % 12)
|
||||
candidates = generator.guitar.chord_positions(required, allowed, bass, prune=prune, profile=profile, melody=melody, root=root)
|
||||
return sorted(candidates, key=lambda shape: (generator.guitar.voicing_cost(shape, profile, allowed, root, melody), shape))
|
||||
requirements = generator.guitar.chord_requirements(definition, profile)
|
||||
candidates = generator.guitar.chord_positions(requirements, prune=prune, profile=profile, melody=melody)
|
||||
return sorted(candidates, key=lambda shape: (generator.guitar.voicing_cost(shape, profile, requirements.allowed, melody), shape))
|
||||
|
||||
class VoicingTests(unittest.TestCase):
|
||||
def test_requested_shapes_win_without_a_shape_dictionary(self):
|
||||
@@ -28,34 +27,37 @@ class VoicingTests(unittest.TestCase):
|
||||
with self.subTest(symbol=symbol):
|
||||
self.assertEqual(ranked(symbol, (melody,))[0], grip(expected))
|
||||
def test_four_strings_precede_melody_height_and_root_presence(self):
|
||||
candidates = ranked('CM7', (64,))
|
||||
four = next(c for c in candidates if len(c) == 4 and max(p.midi for p in c) > 64)
|
||||
three = next(c for c in candidates if len(c) == 3 and max(p.midi for p in c) == 64)
|
||||
candidates = ranked('C#M7', (65,))
|
||||
four = next(c for c in candidates if len(c) == 4 and max(p.midi for p in c) > 65)
|
||||
three = next(c for c in candidates if len(c) == 3 and max(p.midi for p in c) == 65)
|
||||
self.assertLess(candidates.index(four), candidates.index(three))
|
||||
candidates = ranked('FM7', (69,))
|
||||
rootless = next(c for c in candidates if len(c) == 4 and 5 not in {p.midi % 12 for p in c})
|
||||
rooted = next(c for c in candidates if len(c) == 3 and 5 in {p.midi % 12 for p in c})
|
||||
self.assertLess(candidates.index(rootless), candidates.index(rooted))
|
||||
def test_melody_on_top_precedes_root_among_four_string_shapes(self):
|
||||
candidates = ranked('C7b9', (64,))
|
||||
candidates = ranked('C#M7', (65,))
|
||||
best = candidates[0]
|
||||
self.assertEqual(len(best), 4)
|
||||
self.assertEqual(max(p.midi for p in best), 64)
|
||||
self.assertNotIn(0, {p.midi % 12 for p in best})
|
||||
self.assertTrue(any(0 in {p.midi % 12 for p in c} and max(p.midi for p in c) > 64 for c in candidates))
|
||||
def test_root_precedes_physical_cost_without_covering_the_melody(self):
|
||||
self.assertEqual(ranked('CM7', (67,))[0], grip((2, 4, 1, 3)))
|
||||
rooted = generator.guitar.voicing_cost(grip((2, 4, 1, 3)), 'electric', (0, 4, 7, 11), 0, (67,))
|
||||
rootless = generator.guitar.voicing_cost(grip((5, 4, 5, 3)), 'electric', (0, 4, 7, 11), 0, (67,))
|
||||
self.assertGreater(rooted.effort, rootless.effort)
|
||||
self.assertEqual(max(p.midi for p in best), 65)
|
||||
self.assertNotIn(1, {p.midi % 12 for p in best})
|
||||
self.assertTrue(any(1 in {p.midi % 12 for p in c} and max(p.midi for p in c) > 65 for c in candidates))
|
||||
def test_lower_position_can_balance_a_wider_span_without_covering_melody(self):
|
||||
candidates = ranked('CM7', (67,))
|
||||
self.assertEqual(candidates[0], grip((2, 4, 1, 3)))
|
||||
self.assertIn(grip((5, 4, 5, 3)), candidates)
|
||||
rooted = generator.guitar.voicing_cost(grip((2, 4, 1, 3)), 'electric', (0, 4, 7, 11), (67,))
|
||||
rootless = generator.guitar.voicing_cost(grip((5, 4, 5, 3)), 'electric', (0, 4, 7, 11), (67,))
|
||||
self.assertEqual(rooted.effort, rootless.effort)
|
||||
self.assertLess(rooted, rootless)
|
||||
def test_essential_extensions_are_never_relaxed_to_add_the_root(self):
|
||||
for symbol in ('C7b9', 'FM7#11', 'C13', 'Cdim7'):
|
||||
definition = generator.harmony.parse_symbol(symbol)
|
||||
required, allowed, _ = generator.guitar.chord_requirements(definition, 'electric')
|
||||
requirements = generator.guitar.chord_requirements(definition, 'electric')
|
||||
for candidate in ranked(symbol):
|
||||
classes = {p.midi % 12 for p in candidate}
|
||||
self.assertTrue(set(required) <= classes <= set(allowed))
|
||||
self.assertTrue(requirements.covers(classes))
|
||||
self.assertTrue(classes <= set(requirements.allowed))
|
||||
def test_explicitly_removed_root_is_not_restored_or_penalized(self):
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_parts.xml_harmony(kind='major-seventh') + tests.test_musicxml.xml_note('E', octave=5)))
|
||||
measure = document['staves'][0]['measures'][0]
|
||||
@@ -67,34 +69,49 @@ class VoicingTests(unittest.TestCase):
|
||||
_, tones = generator.guitar.guitar_events(document)
|
||||
candidates = list(generator.guitar.attack_candidates([note], tones, {}, {}, False))
|
||||
self.assertTrue(candidates)
|
||||
self.assertTrue(all(c.cost.missing_root == 0 and all(p.midi % 12 != 0 for p in c.shape) for c in candidates))
|
||||
def test_frets_zero_through_twelve_have_no_height_cost(self):
|
||||
for profile in ('', 'electric', 'acoustic'):
|
||||
for fret in range(13):
|
||||
self.assertTrue(all(all(p.midi % 12 != 0 for p in c.shape) and any(p.midi % 12 == 7 for p in c.shape) for c in candidates))
|
||||
def test_chord_height_cost_increases_throughout_the_fretboard(self):
|
||||
for profile in ('electric', 'acoustic'):
|
||||
for fret in range(1, 21):
|
||||
with self.subTest(profile=profile, fret=fret):
|
||||
self.assertEqual(generator.guitar.voicing_cost(grip((fret,) * 4), profile).effort, 0)
|
||||
self.assertEqual(generator.guitar.voicing_cost(grip((fret,) * 4), profile).effort, fret + 2 * max(0, fret - 12))
|
||||
self.assertEqual(generator.guitar.voicing_cost((), profile).effort, 0)
|
||||
def test_height_cost_only_counts_frets_above_twelve(self):
|
||||
for profile in ('', 'electric', 'acoustic'):
|
||||
for fret in range(13, 21):
|
||||
with self.subTest(profile=profile, fret=fret):
|
||||
self.assertEqual(generator.guitar.voicing_cost(grip((fret,) * 4), profile).effort, 2 * (fret - 12))
|
||||
def test_exact_melody_height_scoring_remains_unchanged(self):
|
||||
for fret in range(1, 21):
|
||||
self.assertEqual(generator.guitar.voicing_cost(grip((fret,) * 4)).effort, 2 * max(0, fret - 12))
|
||||
crossing = grip((11, 12, 13, 14))
|
||||
self.assertEqual(generator.guitar.voicing_cost(crossing).effort, 7)
|
||||
self.assertEqual(generator.guitar.voicing_cost(crossing, 'acoustic').effort, 19)
|
||||
self.assertEqual(generator.guitar.voicing_cost(crossing, 'electric').effort, 21)
|
||||
self.assertEqual(generator.guitar.voicing_cost(crossing, 'acoustic').effort, 18)
|
||||
self.assertEqual(generator.guitar.voicing_cost(crossing, 'acoustic').simplicity, 15)
|
||||
def test_span_and_acoustic_simplicity_costs_remain_unchanged(self):
|
||||
shape = grip((0, 2, 3, 4))
|
||||
shape = grip((1, 3, 4, 5))
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape).effort, 4)
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape, 'acoustic').effort, 18)
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape, 'electric').effort, 9)
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape, 'acoustic').effort, 5)
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape, 'acoustic').simplicity, 18)
|
||||
for profile in ('', 'electric', 'acoustic'):
|
||||
self.assertLess(generator.guitar.voicing_cost(grip((8, 8, 8, 8)), profile), generator.guitar.voicing_cost(grip((2, 4, 1, 3)), profile))
|
||||
self.assertLess(generator.guitar.voicing_cost(grip((4, 4, 4, 4)), profile), generator.guitar.voicing_cost(grip((2, 4, 1, 3)), profile))
|
||||
def test_lower_acoustic_half_diminished_and_dominant_grips_win(self):
|
||||
high = [grip((12, 12, 12, 11), (6, 4, 3, 2)), grip((12, 11, 12, 11), (6, 4, 3, 2))]
|
||||
low = [grip((7, 8, 7, 8), (5, 4, 3, 2)), grip((7, 8, 6, 8), (5, 4, 3, 2))]
|
||||
for symbol, upper, lower in zip(('Em7b5', 'A7b9'), high, low):
|
||||
requirements = generator.guitar.chord_requirements(generator.harmony.parse_symbol(symbol), 'acoustic')
|
||||
candidates = generator.guitar.chord_positions(requirements, profile='acoustic')
|
||||
self.assertIn(upper, candidates)
|
||||
self.assertIn(lower, candidates)
|
||||
self.assertEqual({p.midi % 12 for p in upper}, {p.midi % 12 for p in lower})
|
||||
self.assertEqual(min(p.midi for p in upper), min(p.midi for p in lower))
|
||||
self.assertLess(generator.guitar.voicing_cost(lower, 'acoustic', requirements.allowed), generator.guitar.voicing_cost(upper, 'acoustic', requirements.allowed))
|
||||
costs = [sum(generator.guitar.voicing_cost(s, 'acoustic').effort for s in pair) + generator.guitar.acoustic_movement(*pair) for pair in (low, high)]
|
||||
self.assertEqual(costs, [17, 25])
|
||||
def test_acoustic_simpler_three_note_shape_can_beat_four_notes(self):
|
||||
candidates = ranked('C6', profile='acoustic')
|
||||
candidates = ranked('Csus', profile='acoustic')
|
||||
best = candidates[0]
|
||||
self.assertEqual(len(best), 3)
|
||||
self.assertTrue(any(len(c) == 4 for c in candidates))
|
||||
required, _, _ = generator.guitar.chord_requirements(generator.harmony.parse_symbol('C6'), 'acoustic')
|
||||
self.assertTrue(set(required) <= {p.midi % 12 for p in best})
|
||||
requirements = generator.guitar.chord_requirements(generator.harmony.parse_symbol('Csus'), 'acoustic')
|
||||
self.assertTrue(requirements.covers({p.midi % 12 for p in best}))
|
||||
def test_nearby_position_beats_lower_frets_within_comfort_range(self):
|
||||
low, nearby, previous = (grip((f,) * 4) for f in (3, 8, 9))
|
||||
for profile in ('', 'electric', 'acoustic'):
|
||||
@@ -104,7 +121,7 @@ class VoicingTests(unittest.TestCase):
|
||||
for profile in ('', 'electric', 'acoustic'):
|
||||
self.assertLess(generator.guitar.voicing_cost(high, profile), generator.guitar.voicing_cost(low, profile).moved(generator.guitar.movement(high, low)))
|
||||
def test_candidate_pruning_uses_full_cost_and_preserves_each_center(self):
|
||||
for symbol, profile, melody in (('FM7', 'electric', (69,)), ('C7', 'acoustic', ()), ('Gm7', 'electric', (67,))):
|
||||
for symbol, profile, melody in (('FM7', 'electric', (69,)), ('Gm7', 'electric', (67,))):
|
||||
best = {}
|
||||
for shape in ranked(symbol, melody, profile):
|
||||
best.setdefault(generator.guitar.position_center(shape), shape)
|
||||
|
||||
Reference in New Issue
Block a user