+61
-25
@@ -28,9 +28,10 @@ class VoicingTests(unittest.TestCase):
|
||||
for prune in (False, True):
|
||||
candidates = ranked('CM7', prune=prune)
|
||||
self.assertTrue(candidates)
|
||||
self.assertTrue(all(generator.guitar.compactness(c) <= 3 for c in candidates))
|
||||
self.assertTrue(all(generator.guitar.compactness(c) + 1 <= 3 for c in candidates))
|
||||
candidates = ranked('CM7')
|
||||
self.assertIn(grip((2, 4, 1, 3)), candidates)
|
||||
self.assertIn(grip((5, 4, 5, 3)), candidates)
|
||||
self.assertNotIn(grip((2, 4, 1, 3)), candidates)
|
||||
self.assertNotIn(grip((5, 9, 8, 7)), candidates)
|
||||
def test_requested_shapes_win_without_a_shape_dictionary(self):
|
||||
for symbol, melody, expected in (('FM7', 69, (3, 5, 5, 5)), ('Gm7', 67, (3, 3, 3, 3))):
|
||||
@@ -52,14 +53,11 @@ class VoicingTests(unittest.TestCase):
|
||||
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_compact_chord_can_beat_lower_frets_without_covering_melody(self):
|
||||
def test_wider_low_chord_cannot_displace_a_playable_melody_grip(self):
|
||||
candidates = ranked('CM7', (67,))
|
||||
self.assertEqual(candidates[0], grip((5, 4, 5, 3)))
|
||||
self.assertIn(grip((2, 4, 1, 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), (19, 15))
|
||||
self.assertLess(rootless, rooted)
|
||||
self.assertNotIn(grip((2, 4, 1, 3)), candidates)
|
||||
self.assertEqual(max(p.midi for p in candidates[0]), 67)
|
||||
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)
|
||||
@@ -80,36 +78,72 @@ class VoicingTests(unittest.TestCase):
|
||||
candidates = list(generator.guitar.attack_candidates([note], tones, {}, {}, False))
|
||||
self.assertTrue(candidates)
|
||||
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 fret in range(1, 21):
|
||||
def test_anchored_chord_effort_is_independent_of_absolute_height(self):
|
||||
for fret in range(1, 13):
|
||||
with self.subTest(fret=fret):
|
||||
self.assertEqual(generator.guitar.voicing_cost(grip((fret,) * 4), 'electric').effort, fret + 2 * max(0, fret - 12))
|
||||
self.assertEqual(generator.guitar.voicing_cost(grip((fret,) * 4), 'electric').effort, 0)
|
||||
shape = grip((fret, fret + 2, fret + 1, fret + 2))
|
||||
self.assertTrue(generator.fretboard.has_fret_anchor(shape))
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape, 'electric').effort, 10)
|
||||
self.assertEqual(generator.guitar.voicing_cost((), 'electric').effort, 0)
|
||||
def test_electric_anchor_is_eligibility_not_a_highest_fret_cap(self):
|
||||
for prune in (False, True):
|
||||
candidates = ranked('CM7', prune=prune)
|
||||
self.assertTrue(candidates)
|
||||
self.assertTrue(all(min(p.fret for p in c) <= 12 for c in candidates))
|
||||
self.assertIn(grip((14, 12, 12, 12)), candidates)
|
||||
self.assertNotIn(grip((17, 17, 17, 19)), candidates)
|
||||
self.assertFalse(generator.fretboard.has_fret_anchor(grip((13, 13, 13, 13))))
|
||||
self.assertFalse(generator.fretboard.has_fret_anchor(()))
|
||||
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, 'electric').effort, 33)
|
||||
def test_chord_shape_and_height_share_one_effort_cost(self):
|
||||
shape = grip((1, 3, 4, 5))
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape).effort, 4)
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape, 'electric').effort, 23)
|
||||
crossing = grip((12, 13, 14, 14))
|
||||
self.assertEqual(generator.guitar.voicing_cost(crossing).effort, 6)
|
||||
self.assertEqual(generator.guitar.voicing_cost(crossing, 'electric').effort, 10)
|
||||
def test_high_melody_keeps_its_exact_pitch_when_no_anchored_chord_fits(self):
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('C', octave=6)))
|
||||
note = tests.test_musicxml.notes(document)[0]
|
||||
note.update(_harmony=generator.harmony.parse_symbol('CM'), _guitar_profile='electric', _harmonize=True)
|
||||
before = copy.deepcopy(note['pitches'])
|
||||
with self.assertLogs('generator.guitar', level='WARNING') as messages:
|
||||
generator.guitar.place_guitar(document)
|
||||
self.assertIn('1 melody harmonizations kept as single notes', messages.output[0])
|
||||
self.assertEqual([{k: v for k, v in p.items() if k != 'tablature'} for p in note['pitches']], before)
|
||||
self.assertEqual(note['pitches'][0]['tablature'], {'string': 1, 'fret': 20})
|
||||
generator.model.validate_document(document)
|
||||
def test_shape_and_movement_share_one_effort_cost(self):
|
||||
shape = grip((1, 2, 3, 3))
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape).effort, 2)
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape, 'electric').effort, 10)
|
||||
for profile in ('', 'electric'):
|
||||
self.assertLess(generator.guitar.voicing_cost(grip((4, 4, 4, 4)), profile), generator.guitar.voicing_cost(grip((2, 4, 1, 3)), profile))
|
||||
cost = generator.guitar.voicing_cost(shape, profile)
|
||||
self.assertEqual(cost.moved(5).effort, cost.effort + 5)
|
||||
self.assertLess(generator.guitar.voicing_cost(grip((4, 4, 4, 4)), profile), cost)
|
||||
def test_distinct_frets_add_difficulty_even_with_the_same_span(self):
|
||||
simple, varied = grip((3, 3, 5, 5)), grip((3, 4, 5, 5))
|
||||
self.assertEqual(generator.guitar.voicing_cost(simple, 'electric').effort, 13)
|
||||
self.assertEqual(generator.guitar.voicing_cost(varied, 'electric').effort, 15)
|
||||
self.assertEqual(generator.guitar.voicing_cost(simple, 'electric').effort, 8)
|
||||
self.assertEqual(generator.guitar.voicing_cost(varied, 'electric').effort, 10)
|
||||
self.assertEqual(generator.guitar.voicing_cost(simple).effort, generator.guitar.voicing_cost(varied).effort)
|
||||
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'):
|
||||
self.assertLess(generator.guitar.voicing_cost(nearby, profile).moved(generator.guitar.movement(previous, nearby)), generator.guitar.voicing_cost(low, profile).moved(generator.guitar.movement(previous, low)))
|
||||
def test_above_threshold_is_a_soft_cost_not_a_range_priority(self):
|
||||
def test_exact_melody_threshold_remains_a_soft_cost(self):
|
||||
low, high = (grip((f,) * 4) for f in (3, 13))
|
||||
for profile in ('', 'electric'):
|
||||
self.assertLess(generator.guitar.voicing_cost(high, profile), generator.guitar.voicing_cost(low, profile).moved(generator.guitar.movement(high, low)))
|
||||
self.assertLess(generator.guitar.voicing_cost(high), generator.guitar.voicing_cost(low).moved(generator.guitar.movement(high, low)))
|
||||
def test_anchor_is_checked_before_equal_center_pruning(self):
|
||||
# A permissive pitch set lets the ineligible fret-13 barre compete at center 26.
|
||||
requirements = generator.fretboard.ChordRequirements(allowed=tuple(range(12)), foundation=(0,))
|
||||
unpruned = generator.guitar.chord_positions(requirements, prune=False, profile='electric')
|
||||
ineligible = grip((13, 13, 13, 13))
|
||||
center = generator.guitar.position_center(ineligible)
|
||||
eligible = [c for c in unpruned if generator.guitar.position_center(c) == center]
|
||||
self.assertTrue(eligible)
|
||||
self.assertTrue(all(generator.guitar.voicing_cost(ineligible, 'electric', requirements.allowed) < generator.guitar.voicing_cost(c, 'electric', requirements.allowed) for c in eligible))
|
||||
best = min(eligible, key=lambda c: (generator.guitar.voicing_cost(c, 'electric', requirements.allowed), c))
|
||||
self.assertIn(best, generator.guitar.chord_positions(requirements, profile='electric'))
|
||||
self.assertNotIn(ineligible, unpruned)
|
||||
def test_candidate_pruning_uses_full_cost_and_preserves_each_center(self):
|
||||
for symbol, profile, melody in (('FM7', 'electric', (69,)), ('Gm7', 'electric', (67,))):
|
||||
best = {}
|
||||
@@ -161,7 +195,9 @@ class VoicingTests(unittest.TestCase):
|
||||
for note in voice['notes']:
|
||||
frets = [p['tablature']['fret'] for p in note['pitches']]
|
||||
if frets:
|
||||
self.assertLessEqual(max(frets) - min(frets), 3)
|
||||
self.assertLessEqual(max(frets) - min(frets) + 1, 3)
|
||||
if len(frets) > 1:
|
||||
self.assertLessEqual(min(frets), 12)
|
||||
section = measure.get('section_label') or section
|
||||
if section != 'Guitar Melody':
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user