@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user