Fix Playability
ci / checks (push) Failing after 23m26s

This commit is contained in:
khodowany
2026-10-08 19:04:06 -05:00
parent bc7baea7c4
commit 3f63b255ac
23 changed files with 633 additions and 374 deletions
+19 -17
View File
@@ -1,3 +1,4 @@
import generator.hand_position
import generator.validation
import copy
import fractions
@@ -13,6 +14,7 @@ import generator.harmony
import generator.model
import tests.test_acoustic
import tests.test_accompaniment
import tests.position_fixtures
def planned_document(harmonies, attacks=None):
if attacks is None:
@@ -173,19 +175,15 @@ class AcousticVoicingTests(unittest.TestCase):
dense = planned_document(harmonies, [([(i, 1) for i in range(4)], [(i, 1) for i in range(4)])] * 3)
selected = [tuple(tuple(sorted({p for n in span.notes for p in shape(n)})) for span in self.place(document)) for document in (sparse, dense)]
self.assertEqual(*selected)
def test_bass_switch_counts_both_string_and_fret_travel(self):
def test_bass_switch_and_finger_changes_do_not_move_the_hand(self):
previous = voicing((7, 8, 7, 8))
for current, expected in ((voicing((7, 8, 7, 8), 6), 1), (voicing((8, 8, 7, 8), 6), 2), (voicing((9, 8, 7, 8), 6), 3), (voicing((6, 8, 7, 8)), 1)):
self.assertEqual(generator.acoustic_voicing.movement(previous, current), expected)
self.assertEqual(generator.acoustic_voicing.movement(current, previous), expected)
self.assertEqual(generator.acoustic_voicing.movement(previous, previous), 0)
def test_each_upper_string_is_tracked_not_just_the_grip_center(self):
for current in (voicing((7, 8, 7, 8), 6), voicing((8, 8, 7, 8), 6), voicing((9, 8, 7, 8), 6), voicing((6, 8, 7, 8))):
self.assertEqual(generator.acoustic_voicing.movement(previous, current, 6, 6), 0)
self.assertEqual(generator.acoustic_voicing.movement(previous, previous, 5, 7), 2)
def test_each_upper_string_is_tracked_as_secondary_finger_effort(self):
before, after = voicing((5, 7, 5, 7)), voicing((7, 5, 7, 5))
self.assertEqual(generator.acoustic_voicing.movement(before, after), 8)
for index in (1, 2, 3):
frets = [5, 7, 5, 7]
frets[index] += 1
self.assertEqual(generator.acoustic_voicing.movement(before, voicing(frets)), 1)
self.assertEqual(generator.acoustic_voicing.movement(before, after, 5, 5), 0)
self.assertEqual(generator.hand_position.motion(before, after, 5, 5, True).fingers, 8)
def test_fret_and_register_limits_are_hard_constraints(self):
for frets, valid in (((1, 1, 1, 1), True), ((1, 3, 1, 3), True), ((1, 4, 1, 4), True), ((1, 5, 1, 4), False), ((0, 1, 1, 1), False), ((12, 14, 14, 12), True), ((12, 15, 14, 12), True), ((13, 13, 13, 13), False), ((12, 16, 14, 12), False), ((12, 21, 12, 12), False)):
with self.subTest(frets=frets):
@@ -205,7 +203,8 @@ class AcousticVoicingTests(unittest.TestCase):
options = (voicing((1, 1, 1, 1)), voicing((1, 3, 2, 3)), voicing((12, 12, 12, 12)), voicing((12, 14, 13, 14)))
states = [generator.acoustic_voicing.Placement(generator.acoustic_voicing.Cost())]
result = generator.acoustic_voicing.advance(options, generator.fretboard.ChordRequirements(), states)
self.assertEqual([p.cost for p in result], [generator.acoustic_voicing.Cost()] * len(options))
self.assertTrue(all(p.cost.travel == 0 for p in result))
self.assertEqual({(p.voicing, p.hand, p.cost.peak_position) for p in result}, {(v, h, h) for v in options for h in tests.position_fixtures.regions(v)})
def test_optional_tones_break_equal_movement_costs_only(self):
requirements = generator.fretboard.chord_requirements(generator.harmony.parse_symbol('C7'))
states = [generator.acoustic_voicing.Placement(generator.acoustic_voicing.Cost())]
@@ -217,13 +216,16 @@ class AcousticVoicingTests(unittest.TestCase):
def test_dynamic_program_matches_exhaustive_paths_and_is_deterministic(self):
harmonies = ['FM7', 'Gm7', 'C7']
spans = generator.acoustic.harmony_spans(planned_document([[(0, s)] for s in harmonies]))
options = [candidates(s) for s in harmonies]
options = [candidates(s)[:4] for s in harmonies]
requirements = [generator.fretboard.chord_requirements(span.harmony) for span in spans]
costs = [{v: generator.acoustic_voicing.Cost(extended_grips=generator.fretboard.span_penalty(v), missing_tones=len(set(r.allowed) - {p.midi % 12 for p in v})) for v in layer} for r, layer in zip(requirements, options)]
def cost(path):
return generator.acoustic_voicing.Cost(sum(generator.fretboard.span_penalty(v) for v in path), sum(generator.acoustic_voicing.movement(a, b) for a, b in zip(path, path[1:])), sum(len(set(r.allowed) - {p.midi % 12 for p in v}) for r, v in zip(requirements, path)))
selected = generator.acoustic_voicing.select(spans)
self.assertEqual(cost(selected), min(cost(path) for path in itertools.product(*options)))
self.assertEqual(selected, generator.acoustic_voicing.select(spans))
return tests.position_fixtures.cost(path, costs, (1, 2, 3), acoustic=True)
states = [generator.acoustic_voicing.Placement(generator.acoustic_voicing.Cost())]
for layer, r in zip(options, requirements):
states = generator.acoustic_voicing.advance(layer, r, states)
self.assertEqual(min(s.cost for s in states), min(cost(path) for path in itertools.product(*options)))
self.assertEqual(generator.acoustic_voicing.select(spans), generator.acoustic_voicing.select(spans))
def test_complete_triads_and_doubling_use_only_authored_chord_tones(self):
for symbol in ('CM', 'Dm', 'Cdim', 'Caug', 'Dm/F', 'Dm7no7'):
with self.subTest(chord=symbol):