+27
-70
@@ -3,6 +3,7 @@ import pathlib
|
||||
import unittest
|
||||
|
||||
import generator.arrange.generate
|
||||
import generator.fretboard
|
||||
import generator.guitar
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
@@ -13,16 +14,24 @@ import tests.test_musicxml
|
||||
import tests.test_parts
|
||||
|
||||
def grip(frets, strings=(4, 3, 2, 1)):
|
||||
tuning = dict(generator.guitar.STANDARD_TUNING)
|
||||
return tuple(generator.guitar.TabPosition(s, f, tuning[s] + f) for s, f in zip(strings, frets))
|
||||
tuning = dict(generator.fretboard.STANDARD_TUNING)
|
||||
return tuple(generator.fretboard.TabPosition(s, f, tuning[s] + f) for s, f in zip(strings, frets))
|
||||
|
||||
def ranked(symbol, melody=(), profile='electric', prune=False):
|
||||
definition = generator.harmony.parse_symbol(symbol)
|
||||
requirements = generator.guitar.chord_requirements(definition, profile)
|
||||
requirements = generator.fretboard.chord_requirements(definition, respect_bass=profile != 'electric')
|
||||
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_electric_chords_use_the_shared_three_fret_limit(self):
|
||||
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))
|
||||
candidates = ranked('CM7')
|
||||
self.assertIn(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))):
|
||||
with self.subTest(symbol=symbol):
|
||||
@@ -54,7 +63,7 @@ class VoicingTests(unittest.TestCase):
|
||||
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)
|
||||
requirements = generator.guitar.chord_requirements(definition, 'electric')
|
||||
requirements = generator.fretboard.chord_requirements(definition, respect_bass=False)
|
||||
for candidate in ranked(symbol):
|
||||
classes = {p.midi % 12 for p in candidate}
|
||||
self.assertTrue(requirements.covers(classes))
|
||||
@@ -72,91 +81,34 @@ class VoicingTests(unittest.TestCase):
|
||||
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 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, fret + 2 * max(0, fret - 12))
|
||||
self.assertEqual(generator.guitar.voicing_cost((), profile).effort, 0)
|
||||
for fret in range(1, 21):
|
||||
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((), 'electric').effort, 0)
|
||||
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)
|
||||
self.assertEqual(generator.guitar.voicing_cost(crossing, 'acoustic').effort, 24)
|
||||
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)
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape, 'acoustic').effort, 11)
|
||||
for profile in ('', 'electric', 'acoustic'):
|
||||
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))
|
||||
def test_distinct_frets_add_difficulty_even_with_the_same_span(self):
|
||||
simple, varied = grip((3, 3, 5, 5)), grip((3, 4, 5, 5))
|
||||
for profile, expected in (('electric', 13), ('acoustic', 7)):
|
||||
self.assertEqual(generator.guitar.voicing_cost(simple, profile).effort, expected)
|
||||
self.assertEqual(generator.guitar.voicing_cost(varied, profile).effort, expected + 2)
|
||||
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).effort, generator.guitar.voicing_cost(varied).effort)
|
||||
def test_acoustic_reach_excludes_stretched_low_pair_before_scoring(self):
|
||||
high = [grip((12, 12, 12, 11), (6, 4, 3, 2)), grip((12, 11, 12, 11), (6, 4, 3, 2))]
|
||||
requested = [grip((7, 8, 7, 8), (5, 4, 3, 2)), grip((7, 8, 6, 8), (5, 4, 3, 2))]
|
||||
low = [grip((6, 5, 3, 3), (6, 4, 3, 2)), grip((5, 5, 3, 2), (6, 4, 3, 2))]
|
||||
for symbol, upper, middle, lower in zip(('Em7b5', 'A7b9'), high, requested, low):
|
||||
requirements = generator.guitar.chord_requirements(generator.harmony.parse_symbol(symbol), 'acoustic')
|
||||
candidates = generator.guitar.chord_positions(requirements, profile='acoustic')
|
||||
for shape in (upper, middle, lower):
|
||||
self.assertIn(shape, candidates)
|
||||
self.assertEqual({p.midi % 12 for p in upper}, {p.midi % 12 for p in middle})
|
||||
self.assertEqual(min(p.midi for p in upper), min(p.midi for p in middle))
|
||||
preferred = generator.guitar.acoustic_positions(requirements)
|
||||
self.assertIn(middle, preferred)
|
||||
self.assertIn(upper, preferred)
|
||||
self.assertNotIn(lower, preferred)
|
||||
costs = [sum(generator.guitar.voicing_cost(s, 'acoustic').effort for s in pair) + 2 * generator.guitar.acoustic_movement(*pair) for pair in (requested, high, low)]
|
||||
self.assertEqual(costs, [24, 30, 23])
|
||||
def test_real_autumn_leaves_cadences_use_comfortable_inversions_and_complete_triads(self):
|
||||
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/autumn-leaves.xml'))
|
||||
before = copy.deepcopy(source)
|
||||
result = generator.arrange.generate.arrange_document(source, 'acoustic', rhythms=generator.rhythm.load_library(pathlib.Path('data/rhythms')))
|
||||
generator.model.validate_document(result)
|
||||
measures = result['staves'][0]['measures']
|
||||
self.assertEqual(measures[0]['section_label'], 'Rhythm Lead-in')
|
||||
for index, symbol, expected in ((0, 'Em7b5', ((6, 3), (4, 2), (3, 3), (2, 3))), (1, 'A7b9', ((6, 3), (4, 2), (3, 3), (2, 2))), (4, 'Em7b5', ((6, 3), (4, 2), (3, 3), (2, 3))), (5, 'A7b9', ((6, 3), (4, 2), (3, 3), (2, 2)))):
|
||||
measure = measures[index]
|
||||
with self.subTest(measure=measure['number'], chord=symbol):
|
||||
self.assertEqual(generator.harmony.symbol(measure['harmonies'][0]), symbol)
|
||||
positions = {(p['tablature']['string'], p['tablature']['fret']) for v in measure['voices'] for n in v['notes'] for p in n['pitches']}
|
||||
self.assertEqual(tuple(sorted(positions, reverse=True)), expected)
|
||||
checked = set()
|
||||
for measure in measures:
|
||||
if measure['number'] not in ('26', '27', '30', '31', '32', '33'):
|
||||
continue
|
||||
pitches = [p for v in measure['voices'] for n in v['notes'] for p in n['pitches']]
|
||||
if not pitches:
|
||||
continue
|
||||
positions = tuple(generator.guitar.TabPosition(p['tablature']['string'], p['tablature']['fret'], int(generator.model.pitch_number(p)) - 12) for p in pitches)
|
||||
self.assertLessEqual(generator.guitar.physical_reach(positions), generator.guitar.PREFERRED_ACOUSTIC_REACH, measure['id'])
|
||||
self.assertTrue(all('tie_from' not in p and 'tie_to' not in p for p in pitches))
|
||||
if measure['number'] in ('32', '33'):
|
||||
self.assertEqual({p.midi % 12 for p in positions}, {2, 5, 9})
|
||||
checked.add(measure['number'])
|
||||
self.assertEqual(checked, {'26', '27', '30', '31', '32', '33'})
|
||||
self.assertEqual(result, generator.arrange.generate.arrange_document(source, 'acoustic', rhythms=generator.rhythm.load_library(pathlib.Path('data/rhythms'))))
|
||||
self.assertEqual(source, before)
|
||||
def test_acoustic_simpler_three_note_shape_can_beat_four_notes(self):
|
||||
candidates = ranked('Csus', profile='acoustic')
|
||||
best = candidates[0]
|
||||
self.assertEqual(len(best), 3)
|
||||
self.assertTrue(any(len(c) == 4 for c in candidates))
|
||||
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'):
|
||||
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):
|
||||
low, high = (grip((f,) * 4) for f in (3, 13))
|
||||
for profile in ('', 'electric', 'acoustic'):
|
||||
for profile in ('', 'electric'):
|
||||
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,)), ('Gm7', 'electric', (67,))):
|
||||
@@ -205,6 +157,11 @@ class VoicingTests(unittest.TestCase):
|
||||
generator.model.validate_document(result)
|
||||
section, checked = '', set()
|
||||
for measure in result['staves'][0]['measures']:
|
||||
for voice in measure['voices']:
|
||||
for note in voice['notes']:
|
||||
frets = [p['tablature']['fret'] for p in note['pitches']]
|
||||
if frets:
|
||||
self.assertLessEqual(max(frets) - min(frets), 3)
|
||||
section = measure.get('section_label') or section
|
||||
if section != 'Guitar Melody':
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user