Chord Families
This commit is contained in:
@@ -7,6 +7,8 @@ import unittest.mock
|
||||
|
||||
import generator.acoustic
|
||||
import generator.acoustic_voicing
|
||||
import generator.chord_families
|
||||
import generator.comping
|
||||
import generator.electric
|
||||
import generator.fretboard
|
||||
import generator.guitar
|
||||
@@ -18,6 +20,8 @@ import generator.solo.placement
|
||||
import tests.solo_fixtures
|
||||
import tests.test_acoustic_voicing
|
||||
import tests.test_musicxml
|
||||
import tests.test_voicing
|
||||
import tests.score_events
|
||||
|
||||
def position(string, fret):
|
||||
return generator.fretboard.TabPosition(string, fret, dict(generator.fretboard.STANDARD_TUNING)[string] + fret)
|
||||
@@ -35,6 +39,89 @@ def document(positions):
|
||||
return result
|
||||
|
||||
class FretPolicyTests(unittest.TestCase):
|
||||
def test_span_tiers_group_one_two_and_three_positions(self):
|
||||
for frets, span, tier, legal in (((7,), 1, 0, True), ((7, 8), 2, 0, True), ((7, 9), 3, 0, True), ((7, 10), 4, 1, True), ((7, 11), 5, 1, False)):
|
||||
grip = tuple(position(s, f) for s, f in zip((2, 1), frets))
|
||||
self.assertEqual(generator.fretboard.fret_span(grip), span)
|
||||
self.assertEqual(generator.fretboard.span_penalty(grip), tier)
|
||||
self.assertEqual(generator.fretboard.within_fret_span(grip), legal)
|
||||
self.assertEqual(generator.fretboard.fret_span(()), 0)
|
||||
def test_primary_comping_grip_beats_family_and_arbitrarily_less_movement(self):
|
||||
chord = generator.harmony.parse_symbol('C')
|
||||
key = generator.comping.semantic_key(chord)
|
||||
primary = tests.test_acoustic_voicing.voicing((7, 5, 5, 5))
|
||||
fallback = tests.test_acoustic_voicing.voicing((8, 5, 5, 5), 6)
|
||||
self.assertFalse(generator.chord_families.acoustic(chord, primary)['preferred'])
|
||||
self.assertTrue(generator.chord_families.acoustic(chord, fallback)['preferred'])
|
||||
self.assertTrue(all(v in generator.comping.voicings(key, 'acoustic') for v in (primary, fallback)))
|
||||
costs = {v: generator.comping.local_cost('acoustic', key, v, 55) for v in (primary, fallback)}
|
||||
# Movement/family wins cannot purchase entry into the four-fret tier.
|
||||
costs[primary] = costs[primary]._replace(effort=100000)
|
||||
path, cost = generator.comping.select_grips(((primary, fallback),), (costs,), 'acoustic', 'tiers', left=fallback, right=fallback)
|
||||
self.assertEqual(path, (primary,))
|
||||
self.assertEqual(cost.extended_grips, 0)
|
||||
states = [generator.acoustic_voicing.Placement(generator.acoustic_voicing.Cost(), fallback)]
|
||||
legacy = generator.acoustic_voicing.advance((primary, fallback), generator.comping.requirements(chord, 'acoustic'), states)
|
||||
self.assertEqual(min(legacy, key=lambda s: s.cost).voicing, primary)
|
||||
def test_primary_electric_grip_beats_highest_melody_string_and_pruning(self):
|
||||
for prune in (False, True):
|
||||
choices = tests.test_voicing.ranked('G7', (65,), prune=prune)
|
||||
best = choices[0]
|
||||
self.assertEqual(generator.fretboard.span_penalty(best), 0)
|
||||
self.assertEqual(min(p.string_number for p in best if p.midi == 65), 2)
|
||||
self.assertEqual(max(p.midi for p in best), 65)
|
||||
self.assertTrue(any(generator.fretboard.span_penalty(v) and any(p.midi == 65 and p.string_number == 1 for p in v) for v in choices))
|
||||
primary_cost = generator.guitar.voicing_cost(best, 'electric', melody=(65,)).moved(100000)
|
||||
self.assertTrue(all(primary_cost < generator.guitar.voicing_cost(v, 'electric', melody=(65,)) for v in choices if generator.fretboard.span_penalty(v)))
|
||||
def test_previously_missing_chords_select_reported_fallbacks(self):
|
||||
for symbol in ('Bb9', 'BbM9', 'Bb7#9', 'Bb6add9'):
|
||||
with self.subTest(chord=symbol):
|
||||
source = tests.solo_fixtures.song(count=1, chords=(symbol,))
|
||||
canonical = generator.comping.Canonical(source, 'acoustic', 'fallback')
|
||||
report = canonical.report[0]
|
||||
self.assertEqual(report['fret_span'], 4)
|
||||
self.assertEqual(report['span_group'], 'fallback')
|
||||
self.assertEqual(report['span_candidates']['primary'], 0)
|
||||
self.assertGreater(report['span_candidates']['fallback'], 0)
|
||||
self.assertEqual(canonical.path_cost.extended_grips, 1)
|
||||
required = generator.comping.requirements(generator.harmony.parse_symbol(symbol), 'acoustic')
|
||||
self.assertTrue(required.covers({p['sounding_midi'] % 12 for p in report['positions']}))
|
||||
def test_future_held_notes_can_require_a_retained_four_fret_history(self):
|
||||
source = document((position(4, 5), position(2, 2))) # G3, C#4
|
||||
measure = source['staves'][0]['measures'][0]
|
||||
for p in tests.test_musicxml.notes(source)[0]['pitches']:
|
||||
p.pop('tablature')
|
||||
incoming = generator.model.make_event('incoming', fractions.Fraction(1), fractions.Fraction(3), 'note', [{'letter': 'a', 'alter': 0, 'octave': 3, 'tablature': {'string': 3, 'fret': 2}}])
|
||||
measure['voices'].append({'id': 'other', 'notes': [generator.model.make_event('rest', fractions.Fraction(), fractions.Fraction(1), 'rest'), incoming]})
|
||||
# G/C# also have primary placements. Neither permits this held A/string 3.
|
||||
self.assertFalse(generator.fretboard.span_penalty((position(4, 5), position(3, 6))))
|
||||
before = tests.score_events.timeline(source)
|
||||
generator.guitar.place_guitar(source)
|
||||
generator.model.validate_document(source)
|
||||
generator.fretboard.validate_document(source)
|
||||
self.assertEqual(tests.score_events.timeline(source), before)
|
||||
self.assertEqual([p['tablature'] for p in measure['voices'][0]['notes'][0]['pitches']], [{'string': 4, 'fret': 5}, {'string': 2, 'fret': 2}])
|
||||
def test_tied_four_fret_grip_is_charged_once_and_keeps_its_anchor(self):
|
||||
source = document((position(2, 12), position(1, 15)))
|
||||
original = tests.test_musicxml.notes(source)[0]
|
||||
pieces = [generator.model.make_event(f'split_{i}', fractions.Fraction(2 * i), fractions.Fraction(2), 'note', original['pitches']) for i in range(2)]
|
||||
for a, b in zip(pieces[0]['pitches'], pieces[1]['pitches']):
|
||||
a['tie_to'], b['tie_from'] = b['id'], a['id']
|
||||
source['staves'][0]['measures'][0]['voices'][0]['notes'] = pieces
|
||||
_, tones = generator.guitar.guitar_events(source)
|
||||
states = {(): generator.guitar.Placement(generator.guitar.VoicingCost(), (), None, ())}
|
||||
for i, n in enumerate(pieces):
|
||||
states = generator.guitar.advance_positions([n], fractions.Fraction(2 * i), tones, states, True)
|
||||
self.assertEqual(min(s.cost for s in states.values()).extended_grips, 1)
|
||||
generator.guitar.place_guitar(source)
|
||||
generator.model.validate_document(source)
|
||||
generator.fretboard.validate_document(source)
|
||||
self.assertEqual([p['tablature'] for p in pieces[0]['pitches']], [p['tablature'] for p in pieces[1]['pitches']])
|
||||
# Removing the low anchor early still leaves an illegal high single tail.
|
||||
pieces[0]['pitches'][0].pop('tie_to')
|
||||
pieces[1]['pitches'].pop(0)
|
||||
with self.assertRaisesRegex(ValueError, 'held/tied tails'):
|
||||
generator.fretboard.validate_document(source)
|
||||
def test_single_limits_and_alternate_string(self):
|
||||
for midi, expected in ((40, False), (41, True), (72, True), (76, True), (77, False), (84, False)):
|
||||
with self.subTest(midi=midi):
|
||||
@@ -59,7 +146,7 @@ class FretPolicyTests(unittest.TestCase):
|
||||
generator.guitar.place_guitar(source)
|
||||
self.assertEqual(source, before)
|
||||
def test_chord_anchor_and_span_are_hard_constraints(self):
|
||||
for frets, valid in (((12, 14), True), ((13, 14), False), ((12, 15), False), ((0, 2), False)):
|
||||
for frets, valid in (((12, 14), True), ((13, 14), False), ((12, 15), True), ((12, 16), False), ((0, 2), False)):
|
||||
with self.subTest(frets=frets):
|
||||
source = document(tuple(position(s, f) for s, f in zip((2, 1), frets)))
|
||||
if valid:
|
||||
|
||||
Reference in New Issue
Block a user