Span
ci / checks (push) Successful in 48m23s

This commit is contained in:
khodowany
2026-10-01 00:15:35 -05:00
parent 7b60182a3d
commit ce695a626a
10 changed files with 128 additions and 59 deletions
+22 -10
View File
@@ -69,6 +69,7 @@ class AcousticVoicingTests(unittest.TestCase):
requirements = generator.fretboard.chord_requirements(span.harmony)
if basses and uppers:
self.assertTrue(generator.acoustic_voicing.playable(generator.acoustic_voicing.Voicing(*grip)))
self.assertLessEqual(max(p.fret for p in grip) - min(p.fret for p in grip) + 1, 3)
self.assertTrue(requirements.covers({p.midi % 12 for p in grip}))
self.assertTrue({p.midi % 12 for p in grip} <= set(requirements.allowed))
for note in span.notes:
@@ -91,7 +92,7 @@ class AcousticVoicingTests(unittest.TestCase):
self.assertEqual(tests.test_acoustic.sounding(document), before)
generator.model.validate_document(document)
def test_four_strings_allow_all_defined_bass_inversions(self):
for symbol in ('CM', 'Dm', 'Caug', 'Cdim', 'C7', 'Cdim7', 'Cno5', 'C7b9', 'Dmno1', 'C6', 'C13', 'Csus', 'Csus2'):
for symbol in ('CM', 'Dm', 'Caug', 'Cdim', 'C7', 'Cdim7', 'C7no5', 'C7b9', 'C7no1', 'C6', 'C13', 'Csus', 'Csus2'):
with self.subTest(chord=symbol):
chord = generator.harmony.parse_symbol(symbol)
requirements = generator.fretboard.chord_requirements(chord)
@@ -188,13 +189,19 @@ class AcousticVoicingTests(unittest.TestCase):
frets[index] += 1
self.assertEqual(generator.acoustic_voicing.movement(before, voicing(frets)), 1)
def test_fret_and_register_limits_are_hard_constraints(self):
for frets, valid in (((1, 1, 1, 1), True), ((1, 4, 1, 4), True), ((1, 5, 1, 4), False), ((0, 1, 1, 1), False), ((12, 15, 14, 12), True), ((13, 13, 13, 13), False), ((12, 16, 14, 12), False), ((12, 21, 12, 12), False)):
for frets, valid in (((1, 1, 1, 1), True), ((1, 3, 1, 3), True), ((1, 4, 1, 4), False), ((1, 5, 1, 4), False), ((0, 1, 1, 1), False), ((12, 14, 14, 12), True), ((12, 15, 14, 12), False), ((13, 13, 13, 13), False), ((12, 16, 14, 12), False), ((12, 21, 12, 12), False)):
with self.subTest(frets=frets):
self.assertEqual(generator.acoustic_voicing.playable(voicing(frets)), valid)
for symbol in ('CM', 'C7b9', 'C13', 'Cdim7'):
self.assertTrue(all(generator.acoustic_voicing.playable(v) for v in candidates(symbol)))
def test_autumn_leaves_cm7_and_f7_cannot_span_four_fret_positions(self):
for symbol, frets in (('Cm7', (3, 1, 3, 4)), ('F7', (3, 1, 2, 4))):
with self.subTest(symbol=symbol):
self.assertFalse(generator.acoustic_voicing.playable(voicing(frets)))
self.assertNotIn(voicing(frets), candidates(symbol))
self.place(planned_document([[(0, symbol)]]))
def test_acoustic_cost_has_no_register_span_or_shape_penalties(self):
options = (voicing((1, 1, 1, 1)), voicing((1, 4, 3, 4)), voicing((12, 12, 12, 12)), voicing((12, 15, 14, 15)))
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))
@@ -223,16 +230,18 @@ class AcousticVoicingTests(unittest.TestCase):
options = candidates(symbol)
self.assertTrue(options)
self.assertTrue(all(len(v) == 4 and {p.midi % 12 for p in v} == classes for v in options))
for symbol, omitted in (('Dmno1', 2), ('C7no5', 7)):
for symbol, omitted in (('C7no1', 0), ('C7no5', 7)):
self.assertTrue(candidates(symbol))
self.assertTrue(all(omitted not in {p.midi % 12 for p in v} for v in candidates(symbol)))
def test_unplayable_omission_is_not_restored_to_fill_upper_strings(self):
self.assertFalse(candidates('Dmno5'))
document = planned_document([[(0, 'Dmno5')]])
before = copy.deepcopy(document)
with self.assertRaisesRegex(ValueError, 'Dmno5: no acoustic grip.*three-fret span'):
generator.acoustic_voicing.place(document)
self.assertEqual(document, before)
for symbol in ('Dmno5', 'Dmno1', 'CMno5'):
with self.subTest(symbol=symbol):
self.assertFalse(candidates(symbol))
document = planned_document([[(0, symbol)]])
before = copy.deepcopy(document)
with self.assertRaisesRegex(ValueError, f'{symbol}: no acoustic grip.*3 consecutive fret positions'):
generator.acoustic_voicing.place(document)
self.assertEqual(document, before)
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), ('C7/E', 4), ('C7b9/Db', 1), ('Dm/B', 11)):
with self.subTest(chord=chord):
@@ -267,6 +276,9 @@ class AcousticVoicingTests(unittest.TestCase):
for note in voice['notes']:
upper = sorted(p['tablature']['string'] for p in note['pitches'] if p['tablature']['string'] <= 4)
self.assertIn(upper, ([], [2, 3, 4]))
frets = [p['tablature']['fret'] for p in note['pitches']]
if frets:
self.assertLessEqual(max(frets) - min(frets) + 1, 3)
self.assertTrue(all('tie_from' not in p and 'tie_to' not in p for p in note['pitches']))
if path.stem == 'autumn_leaves':
for index, expected in ((0, voicing((3, 2, 3, 3), 6)), (1, voicing((3, 2, 3, 2), 6)), (4, voicing((3, 2, 3, 3), 6)), (5, voicing((3, 2, 3, 2), 6))):
+4 -4
View File
@@ -117,10 +117,10 @@ class EngravingTests(unittest.TestCase):
self.assert_paired_pages(self.layout(source))
def test_two_voice_acoustic_systems_can_share_a_page(self):
harmony = '<harmony><root><root-step>E</root-step></root><kind>major</kind></harmony>'
content = harmony + tests.test_musicxml.xml_note('G', 2, 'half', alter=1) + tests.test_musicxml.xml_note('B', 2, 'half', prefix='<chord/>') + '<note><rest/><duration>2</duration><type>half</type></note>'
content += '<backup><duration>4</duration></backup>' + tests.test_musicxml.xml_note('E', extra='<voice>2</voice>')
content = harmony + tests.test_musicxml.xml_note('G', 2, 'half', alter=1) + tests.test_musicxml.xml_note('E', 2, 'half', prefix='<chord/>') + '<note><rest/><duration>2</duration><type>half</type></note>'
content += '<backup><duration>4</duration></backup>' + tests.test_musicxml.xml_note('B', octave=3, extra='<voice>2</voice>')
document = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([content] * 16))
positions = {'e': {'string': 5, 'fret': 7}, 'g': {'string': 4, 'fret': 6}, 'b': {'string': 3, 'fret': 4}}
positions = {'b': {'string': 5, 'fret': 2}, 'e': {'string': 4, 'fret': 2}, 'g': {'string': 3, 'fret': 1}}
for measure in document['staves'][0]['measures']:
measure['transpose']['octave_change'] = -1
for voice in measure['voices']:
@@ -413,7 +413,7 @@ class EngravingTests(unittest.TestCase):
self.assertAlmostEqual(x1, x2)
self.assertAlmostEqual((y1 - baselines['staff']) * 5 * layout.style.tab_space / (4 * layout.style.space), y2 - baselines['tab_staff'])
def test_adjacent_string_tab_numerals_do_not_overlap(self):
content = ''.join(tests.test_musicxml.xml_note(step, octave=octave, prefix='<chord/>' if index else '') for index, (step, octave) in enumerate((('G', 3), ('C', 4), ('E', 4), ('G', 4), ('C', 5), ('E', 5))))
content = ''.join(tests.test_musicxml.xml_note(step, octave=octave, prefix='<chord/>' if index else '') for index, (step, octave) in enumerate((('E', 3), ('B', 3), ('E', 4), ('G', 4), ('B', 4), ('E', 5))))
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content))
generator.guitar.place_guitar(source)
layout = self.layout(source)
+19 -4
View File
@@ -29,11 +29,12 @@ class GuitarTests(unittest.TestCase):
self.assertIn(generator.fretboard.TabPosition(1, 20, 84), generator.fretboard.positions_for_pitch(fractions.Fraction(84)))
self.assertFalse(generator.fretboard.positions_for_pitch(fractions.Fraction(85)))
self.assertFalse(generator.fretboard.positions_for_pitch(fractions.Fraction(129, 2)))
self.assertTrue(generator.fretboard.within_fret_span((generator.fretboard.TabPosition(4, 11, 61), generator.fretboard.TabPosition(3, 12, 67), generator.fretboard.TabPosition(2, 14, 73))))
self.assertTrue(generator.fretboard.within_fret_span((generator.fretboard.TabPosition(4, 11, 61), generator.fretboard.TabPosition(3, 12, 67), generator.fretboard.TabPosition(2, 13, 72))))
self.assertFalse(generator.fretboard.within_fret_span((generator.fretboard.TabPosition(4, 11, 61), generator.fretboard.TabPosition(3, 12, 67), generator.fretboard.TabPosition(2, 14, 73))))
self.assertFalse(generator.fretboard.within_fret_span((generator.fretboard.TabPosition(6, 0, 40), generator.fretboard.TabPosition(1, 20, 84))))
self.assertFalse(generator.fretboard.within_fret_span((generator.fretboard.TabPosition(4, 10, 60), generator.fretboard.TabPosition(1, 20, 84))))
def test_fretted_positions_keep_span_and_proximity_scoring(self):
for frets, valid in (((1, 2, 3), True), ((1, 4), True), ((1, 5), False), ((7, 10), True), ((7, 11), False), ((12, 1, 11), False), ((3, 3), True)):
for frets, valid in (((1, 2, 3), True), ((1, 4), False), ((1, 5), False), ((7, 9), True), ((7, 10), False), ((7, 11), False), ((12, 1, 11), False), ((3, 3), True), ((18, 20), True)):
with self.subTest(frets=frets):
positions = tuple(generator.fretboard.TabPosition(6 - i, fret, 0) for i, fret in enumerate(frets))
self.assertEqual(generator.fretboard.within_fret_span(positions), valid)
@@ -42,7 +43,7 @@ class GuitarTests(unittest.TestCase):
self.assertTrue(generator.fretboard.within_fret_span(()))
def test_held_notes_obey_the_shared_three_fret_limit(self):
held = {'held': generator.fretboard.TabPosition(4, 7, 57)}
for fret, valid in ((10, True), (11, False)):
for fret, valid in ((9, True), (10, False), (11, False)):
with self.subTest(fret=fret):
pending = [('attack', (generator.fretboard.TabPosition(2, fret, 59 + fret),))]
self.assertEqual(bool(list(generator.guitar.extend_positions(held, pending))), valid)
@@ -179,13 +180,21 @@ class GuitarTests(unittest.TestCase):
self.assertTrue(all(m['transpose']['octave_change'] == -1 for m in document['staves'][0]['measures']))
def test_ties_and_overlapping_voices_keep_strings(self):
content = tests.test_musicxml.xml_note('E', 1, 'quarter', octave=4, extra='<tie type="start"/>') + tests.test_musicxml.xml_note('E', 3, 'half', octave=4, extra='<dot/><tie type="stop"/>')
content += '<backup><duration>4</duration></backup><forward><duration>1</duration></forward>' + tests.test_musicxml.xml_note('F', 3, 'half', octave=4, extra='<dot/><voice>2</voice>')
content += '<backup><duration>4</duration></backup><forward><duration>1</duration></forward>' + tests.test_musicxml.xml_note('G', 3, 'half', octave=4, extra='<dot/><voice>2</voice>')
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content))
generator.guitar.place_guitar(document)
first, continuation, other = [p['tablature'] for m in document['staves'][0]['measures'] for v in m['voices'] for n in v['notes'] for p in n['pitches']]
self.assertEqual(first, continuation)
self.assertNotEqual(continuation['string'], other['string'])
self.assertEqual(len(generator.playback.compile_timeline(document)['events']), 2)
def test_overlapping_tones_cannot_expand_the_span_or_relocate_a_tie(self):
content = tests.test_musicxml.xml_note('E', 1, 'quarter', octave=4, extra='<tie type="start"/>') + tests.test_musicxml.xml_note('E', 3, 'half', octave=4, extra='<dot/><tie type="stop"/>')
content += '<backup><duration>4</duration></backup><forward><duration>1</duration></forward>' + tests.test_musicxml.xml_note('F', 3, 'half', octave=4, extra='<dot/><voice>2</voice>')
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content))
before = copy.deepcopy(document)
with self.assertRaisesRegex(ValueError, '3 consecutive fret positions.*held/tied strings'):
generator.guitar.place_guitar(document)
self.assertEqual(document, before)
def test_proximity_chooses_a_path_without_relocating_tied_notes(self):
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('B', 2, 'half', octave=5) + tests.test_musicxml.xml_note('C', 2, 'half', octave=6)))
generator.guitar.place_guitar(source)
@@ -263,3 +272,9 @@ class GuitarTests(unittest.TestCase):
generator.__main__.generate_assets(args)
self.assertEqual([p.name for p in output.iterdir()], ['last-good'])
self.assertEqual((output / 'last-good').read_text(), 'preserved')
harmony = '<harmony><root><root-step>C</root-step></root><kind>power</kind></harmony>'
(root / 'data/songs/0001.xml').write_text(tests.test_musicxml.xml_score(harmony + tests.test_musicxml.xml_note()))
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), self.assertRaisesRegex(ValueError, '0001.xml, part acoustic:.*3 consecutive fret positions'):
generator.__main__.generate_assets(args)
self.assertEqual([p.name for p in output.iterdir()], ['last-good'])
self.assertEqual((output / 'last-good').read_text(), 'preserved')
+6 -1
View File
@@ -25,7 +25,7 @@ class GuitarPartTests(unittest.TestCase):
generator.model.validate_document(result)
return result
def test_profile_shapes_cover_defining_chord_tones(self):
for symbol in ('CM', 'Cm', 'C7', 'CmM7', 'Csus', 'Cdim7', 'Caug7', 'C7b9', 'C13', 'C5', 'CM/E'):
for symbol in ('CM', 'Cm', 'C7', 'CmM7', 'Csus', 'Cdim7', 'Caug7', 'C7b9', 'C13', 'CM/E'):
for profile in ('acoustic', 'electric'):
with self.subTest(symbol=symbol, profile=profile):
definition = generator.harmony.parse_symbol(symbol)
@@ -43,6 +43,11 @@ class GuitarPartTests(unittest.TestCase):
self.assertNotIn(0, generator.fretboard.chord_requirements(chord, respect_bass=False).required)
candidates = generator.guitar.chord_positions(generator.fretboard.chord_requirements(chord, respect_bass=False), profile='electric')
self.assertTrue(any(all(p.midi % 12 != 0 for p in c) for c in candidates))
def test_power_chord_cannot_expand_span_or_invent_upper_tones(self):
requirements = generator.fretboard.chord_requirements(generator.harmony.parse_symbol('C5'))
self.assertEqual(set(requirements.allowed), {0, 7})
self.assertFalse(generator.acoustic_voicing.candidates(requirements))
self.assertFalse(generator.guitar.chord_positions(requirements, profile='electric'))
def test_acoustic_combines_independent_rhythms_with_a_fixed_grip(self):
result = self.section(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(), 'acoustic')
measure = result['staves'][0]['measures'][0]
+61 -25
View File
@@ -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