@@ -1,6 +1,7 @@
|
||||
import copy
|
||||
import fractions
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
import generator.accompaniment
|
||||
import generator.acoustic
|
||||
@@ -85,9 +86,9 @@ class AcousticVoicingTests(unittest.TestCase):
|
||||
root_shapes = generator.guitar.chord_positions(generator.guitar.chord_requirements(generator.harmony.parse_symbol('Csus/C'), 'acoustic'), profile='acoustic')
|
||||
selected = tuple(sorted({p for n in spans[0].notes for p in shape(n)}, reverse=True))
|
||||
self.assertLessEqual(generator.guitar.voicing_cost(selected, 'acoustic'), min(generator.guitar.voicing_cost(s, 'acoustic') for s in root_shapes))
|
||||
def test_bass_options_follow_defined_fifth_and_explicit_basses(self):
|
||||
def test_bass_options_follow_all_chord_tones_and_explicit_basses(self):
|
||||
inversion = generator.harmony.parse_symbol('Cdim7') | {'inversion': 0}
|
||||
for chord, expected in (('C7', {0, 7}), ('Cdim7', {0, 6}), ('Caug', {0, 8}), ('Cno5', {0}), ('C7/E', {4}), (inversion, {0})):
|
||||
for chord, expected in (('C7', {0, 4, 7, 10}), ('Cdim7', {0, 3, 6, 9}), ('Caug', {0, 4, 8}), ('Cno5', {0, 4}), ('C7b9', {0, 1, 4, 7, 10}), ('Dmno1', {5, 9}), ('C7/E', {4}), (inversion, {0})):
|
||||
with self.subTest(chord=chord):
|
||||
span = generator.acoustic.harmony_spans(planned_document([[(0, chord)]]))[0]
|
||||
candidates = list(generator.guitar.acoustic_candidates(span))
|
||||
@@ -116,8 +117,9 @@ class AcousticVoicingTests(unittest.TestCase):
|
||||
spans = self.place(document)
|
||||
self.assertEqual(len(spans), 4)
|
||||
basses = [min(p.midi for n in span.notes for p in shape(n)) % 12 for span in spans]
|
||||
self.assertIn(basses[0], (0, 7))
|
||||
self.assertEqual(basses[1:], [4, 7, 6])
|
||||
self.assertIn(basses[0], (0, 4, 7, 10))
|
||||
self.assertEqual(basses[1:3], [4, 7])
|
||||
self.assertIn(basses[3], (0, 4, 6, 10))
|
||||
def test_non_accompaniment_passage_ends_the_span(self):
|
||||
document = planned_document([[(0, 'C7')], [], []])
|
||||
measure = document['staves'][0]['measures'][1]
|
||||
@@ -189,3 +191,103 @@ class AcousticVoicingTests(unittest.TestCase):
|
||||
candidates = generator.guitar.chord_positions(requirements, profile='acoustic')
|
||||
self.assertEqual(candidates, generator.guitar.chord_positions(requirements, profile='acoustic', prune=False))
|
||||
self.assertGreater(len(candidates), len({generator.guitar.position_center(p) for p in candidates}))
|
||||
def test_physical_reach_uses_fret_spacing_and_an_inclusive_reference(self):
|
||||
tuning = dict(generator.guitar.STANDARD_TUNING)
|
||||
low = tuple(generator.guitar.TabPosition(s, f, tuning[s] + f) for s, f in ((5, 3), (3, 2), (2, 5)))
|
||||
reference = tuple(generator.guitar.TabPosition(s, f, tuning[s] + f) for s, f in ((5, 8), (3, 7), (2, 10)))
|
||||
high = tuple(generator.guitar.TabPosition(s, f, tuning[s] + f) for s, f in ((5, 9), (3, 8), (2, 11)))
|
||||
self.assertEqual(generator.guitar.compactness(low), generator.guitar.compactness(reference))
|
||||
self.assertGreater(generator.guitar.physical_reach(low), generator.guitar.physical_reach(reference))
|
||||
self.assertGreater(generator.guitar.physical_reach(reference), generator.guitar.physical_reach(high))
|
||||
self.assertEqual(generator.guitar.physical_reach(reference), generator.guitar.PREFERRED_ACOUSTIC_REACH)
|
||||
self.assertEqual(generator.guitar.physical_reach(()), 0)
|
||||
self.assertEqual(generator.guitar.physical_reach(reference[:1]), 0)
|
||||
span = generator.acoustic.harmony_spans(planned_document([[(0, 'Dm')]]))[0]
|
||||
self.assertIn(reference, [c.shape for c in generator.guitar.acoustic_candidates(span)])
|
||||
def test_acoustic_effort_does_not_reward_further_compression(self):
|
||||
tuning = dict(generator.guitar.STANDARD_TUNING)
|
||||
shapes = [tuple(generator.guitar.TabPosition(s, f, tuning[s] + f) for s, f in zip((5, 4, 3, 2), frets)) for frets in ((5, 5, 7, 7), (6, 6, 7, 7))]
|
||||
self.assertGreater(generator.guitar.physical_reach(shapes[0]), generator.guitar.physical_reach(shapes[1]))
|
||||
self.assertTrue(all(generator.guitar.physical_reach(s) <= generator.guitar.PREFERRED_ACOUSTIC_REACH for s in shapes))
|
||||
self.assertEqual(*(generator.guitar.voicing_cost(s, 'acoustic').effort for s in shapes))
|
||||
def test_comfortable_candidates_win_even_when_previous_grip_is_wider(self):
|
||||
tuning = dict(generator.guitar.STANDARD_TUNING)
|
||||
wider = tuple(generator.guitar.TabPosition(s, f, tuning[s] + f) for s, f in ((6, 6), (4, 5), (3, 3), (2, 3)))
|
||||
active = tuple((str(p.string_number), p) for p in wider)
|
||||
states = {active: generator.guitar.Placement(generator.guitar.VoicingCost(), active, None, ())}
|
||||
span = generator.acoustic.harmony_spans(planned_document([[(0, 'Em7b5')]]))[0]
|
||||
requirements = generator.guitar.chord_requirements(span.harmony, 'acoustic')
|
||||
self.assertIn(wider, generator.guitar.chord_positions(requirements, profile='acoustic'))
|
||||
candidates = list(generator.guitar.acoustic_candidates(span))
|
||||
self.assertNotIn(wider, [c.shape for c in candidates])
|
||||
result = generator.guitar.advance_acoustic(candidates, states)
|
||||
chosen = min(result.values(), key=lambda p: (p.cost, p.active))
|
||||
self.assertLessEqual(generator.guitar.physical_reach(tuple(p for _, p in chosen.active)), generator.guitar.PREFERRED_ACOUSTIC_REACH)
|
||||
def test_complete_triads_are_preferred_within_comfortable_candidates(self):
|
||||
for symbol in ('CM', 'Dm', 'Cdim', 'Caug', 'Dm/F'):
|
||||
with self.subTest(chord=symbol):
|
||||
span = generator.acoustic.harmony_spans(planned_document([[(0, symbol)]]))[0]
|
||||
candidates = list(generator.guitar.acoustic_candidates(span))
|
||||
classes = set(generator.harmony.pitch_classes(span.harmony))
|
||||
self.assertTrue(candidates)
|
||||
self.assertTrue(all(classes <= {p.midi % 12 for p in c.shape} for c in candidates))
|
||||
self.assertTrue(all(generator.guitar.physical_reach(c.shape) <= generator.guitar.PREFERRED_ACOUSTIC_REACH for c in candidates))
|
||||
def test_omissions_and_extended_chords_do_not_gain_complete_triad_requirement(self):
|
||||
for symbol, omitted in (('Dmno1', 2), ('Dmno5', 9)):
|
||||
with self.subTest(chord=symbol):
|
||||
span = generator.acoustic.harmony_spans(planned_document([[(0, symbol)]]))[0]
|
||||
candidates = list(generator.guitar.acoustic_candidates(span))
|
||||
self.assertTrue(candidates)
|
||||
self.assertTrue(all(omitted not in {p.midi % 12 for p in c.shape} for c in candidates))
|
||||
for symbol in ('C7', 'C7b9', 'C13'):
|
||||
with self.subTest(chord=symbol):
|
||||
span = generator.acoustic.harmony_spans(planned_document([[(0, symbol)]]))[0]
|
||||
candidates = list(generator.guitar.acoustic_candidates(span))
|
||||
requirements = generator.guitar.chord_requirements(span.harmony, 'acoustic')
|
||||
self.assertTrue(any(0 not in {p.midi % 12 for p in c.shape} for c in candidates))
|
||||
self.assertTrue(all(requirements.covers({p.midi % 12 for p in c.shape}) for c in candidates))
|
||||
def test_complete_triad_preference_cannot_force_a_wider_grip(self):
|
||||
tuning = dict(generator.guitar.STANDARD_TUNING)
|
||||
reduced, complete = [tuple(generator.guitar.TabPosition(s, f, tuning[s] + f) for s, f in pairs) for pairs in (((6, 1), (4, 3), (3, 2)), ((5, 5), (4, 3), (3, 2)))]
|
||||
requirements = generator.guitar.chord_requirements(generator.harmony.parse_symbol('Dm'), 'acoustic')
|
||||
self.assertTrue(all(requirements.covers({p.midi % 12 for p in s}) for s in (reduced, complete)))
|
||||
self.assertLessEqual(generator.guitar.physical_reach(reduced), generator.guitar.PREFERRED_ACOUSTIC_REACH)
|
||||
self.assertGreater(generator.guitar.physical_reach(complete), generator.guitar.PREFERRED_ACOUSTIC_REACH)
|
||||
generator.guitar.acoustic_positions.cache_clear()
|
||||
self.addCleanup(generator.guitar.acoustic_positions.cache_clear)
|
||||
with unittest.mock.patch('generator.guitar.chord_positions', return_value=(reduced, complete)):
|
||||
self.assertEqual(generator.guitar.acoustic_positions(requirements, complete_triad=True), (reduced,))
|
||||
def test_explicit_inversions_and_nonchord_slash_basses_are_preserved(self):
|
||||
for chord, bass in ((generator.harmony.parse_symbol('Dm') | {'inversion': 1}, 5), (generator.harmony.parse_symbol('C7') | {'inversion': 3}, 10), ('Dm/B', 11)):
|
||||
with self.subTest(chord=chord):
|
||||
document = planned_document([[(0, chord)]])
|
||||
span = self.place(document)[0]
|
||||
self.assertEqual({p.midi % 12 for n in span.notes for p in shape(n) if p.string_number >= 5}, {bass})
|
||||
def test_wider_fallback_is_least_reaching_and_reported_with_context(self):
|
||||
document = planned_document([[(0, 'C13/E')]])
|
||||
span = generator.acoustic.harmony_spans(document)[0]
|
||||
requirements = generator.guitar.chord_requirements(span.harmony, 'acoustic')
|
||||
raw = generator.guitar.chord_positions(requirements, profile='acoustic')
|
||||
minimum = min(generator.guitar.physical_reach(s) for s in raw)
|
||||
self.assertGreater(minimum, generator.guitar.PREFERRED_ACOUSTIC_REACH)
|
||||
with self.assertLogs('generator.guitar', level='WARNING') as logs:
|
||||
candidates = list(generator.guitar.acoustic_candidates(span))
|
||||
self.assertTrue(candidates)
|
||||
self.assertEqual({generator.guitar.physical_reach(c.shape) for c in candidates}, {minimum})
|
||||
self.assertTrue(all(requirements.covers({p.midi % 12 for p in c.shape}) and generator.guitar.within_fret_span(c.shape) for c in candidates))
|
||||
self.assertIn(span.location, logs.output[0])
|
||||
self.assertIn('C13/E requires a wider acoustic reach', logs.output[0])
|
||||
with self.assertLogs('generator.guitar', level='WARNING'):
|
||||
self.place(document)
|
||||
def test_inversions_still_follow_nearby_full_grip_positions(self):
|
||||
tuning = dict(generator.guitar.STANDARD_TUNING)
|
||||
for previous, expected in ((((6, 3), (4, 2), (3, 3), (2, 3)), ((6, 3), (4, 2), (3, 3), (2, 2))), (((5, 7), (4, 8), (3, 7), (2, 8)), ((5, 7), (4, 8), (3, 6), (2, 8)))):
|
||||
with self.subTest(previous=previous):
|
||||
grip = tuple(generator.guitar.TabPosition(s, f, tuning[s] + f) for s, f in previous)
|
||||
active = tuple((str(p.string_number), p) for p in grip)
|
||||
states = {active: generator.guitar.Placement(generator.guitar.VoicingCost(), active, None, ())}
|
||||
span = generator.acoustic.harmony_spans(planned_document([[(0, 'A7b9')]]))[0]
|
||||
result = generator.guitar.advance_acoustic(generator.guitar.acoustic_candidates(span), states)
|
||||
chosen = tuple(p for _, p in min(result.values(), key=lambda p: (p.cost, p.active)).active)
|
||||
self.assertEqual(tuple((p.string_number, p.fret) for p in chosen), expected)
|
||||
self.assertEqual(generator.guitar.acoustic_movement(grip, chosen), 1)
|
||||
|
||||
+33
-13
@@ -82,22 +82,22 @@ class VoicingTests(unittest.TestCase):
|
||||
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)
|
||||
for profile in ('electric', 'acoustic'):
|
||||
self.assertEqual(generator.guitar.voicing_cost(crossing, profile).effort, 33)
|
||||
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)
|
||||
for profile in ('electric', 'acoustic'):
|
||||
self.assertEqual(generator.guitar.voicing_cost(shape, profile).effort, 23)
|
||||
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'):
|
||||
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 in ('electric', 'acoustic'):
|
||||
self.assertEqual(generator.guitar.voicing_cost(simple, profile).effort, 13)
|
||||
self.assertEqual(generator.guitar.voicing_cost(varied, profile).effort, 15)
|
||||
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).effort, generator.guitar.voicing_cost(varied).effort)
|
||||
def test_compact_mid_neck_acoustic_pair_beats_high_and_stretched_low_pairs(self):
|
||||
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))]
|
||||
@@ -108,20 +108,40 @@ class VoicingTests(unittest.TestCase):
|
||||
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, [33, 36, 41])
|
||||
def test_real_autumn_leaves_lead_in_uses_requested_compact_grips(self):
|
||||
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'][:2]
|
||||
measures = result['staves'][0]['measures']
|
||||
self.assertEqual(measures[0]['section_label'], 'Rhythm Lead-in')
|
||||
for measure, symbol, expected in zip(measures, ('Em7b5', 'A7b9'), (((5, 7), (4, 8), (3, 7), (2, 8)), ((5, 7), (4, 8), (3, 6), (2, 8)))):
|
||||
with self.subTest(chord=symbol):
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user