Voice Leading
ci / checks (push) Successful in 48m12s

This commit is contained in:
khodowany
2026-09-30 15:13:00 -05:00
parent 10d295abfe
commit fb92ed78ee
17 changed files with 511 additions and 484 deletions
+16 -14
View File
@@ -3,7 +3,9 @@ import fractions
import unittest
import generator.accompaniment
import generator.acoustic_voicing
import generator.arrange.generate
import generator.fretboard
import generator.guitar
import generator.harmony
import generator.model
@@ -27,19 +29,19 @@ class GuitarPartTests(unittest.TestCase):
for profile in ('acoustic', 'electric'):
with self.subTest(symbol=symbol, profile=profile):
definition = generator.harmony.parse_symbol(symbol)
requirements = generator.guitar.chord_requirements(definition, profile)
candidates = generator.guitar.chord_positions(requirements, profile=profile)
requirements = generator.fretboard.chord_requirements(definition, respect_bass=profile != 'electric')
candidates = generator.acoustic_voicing.candidates(requirements) if profile == 'acoustic' else generator.guitar.chord_positions(requirements, profile=profile)
self.assertTrue(candidates)
for candidate in candidates:
strings = tuple(p.string_number for p in candidate)
self.assertIn(strings, generator.guitar.ACOUSTIC_STRINGS if profile == 'acoustic' else generator.guitar.ELECTRIC_STRINGS)
self.assertIn(strings, ((5, 4, 3, 2), (6, 4, 3, 2)) if profile == 'acoustic' else generator.guitar.ELECTRIC_STRINGS)
self.assertTrue(requirements.covers({p.midi % 12 for p in candidate}))
self.assertTrue({p.midi % 12 for p in candidate} <= set(requirements.allowed))
self.assertTrue(generator.guitar.within_fret_span(candidate))
self.assertTrue(generator.fretboard.within_fret_span(candidate))
self.assertTrue(all(1 <= p.fret <= 20 for p in candidate))
chord = generator.harmony.parse_symbol('C7')
self.assertNotIn(0, generator.guitar.chord_requirements(chord, 'electric').required)
candidates = generator.guitar.chord_positions(generator.guitar.chord_requirements(chord, 'electric'), profile='electric')
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_acoustic_combines_independent_rhythms_with_a_fixed_grip(self):
result = self.section(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(), 'acoustic')
@@ -59,17 +61,17 @@ class GuitarPartTests(unittest.TestCase):
self.assertNotIn('stem', note)
strings = sorted(p['tablature']['string'] for p in note['pitches'] if p['tablature']['string'] <= 4)
if strings:
self.assertIn(strings, ([2, 3], [3, 4], [2, 3, 4]))
self.assertEqual(strings, [2, 3, 4])
timeline = generator.playback.compile_timeline(result)['events']
by_id = {p['id']: p for v in measure['voices'] for n in v['notes'] for p in n['pitches']}
for event in timeline:
start = generator.model.fraction(event['onset'])
simultaneous = [e for e in timeline if generator.model.fraction(e['onset']) <= start < generator.model.fraction(e['onset']) + generator.model.fraction(e['duration'])]
positions = [generator.guitar.TabPosition(by_id[e['id']]['tablature']['string'], by_id[e['id']]['tablature']['fret'], int(generator.model.fraction(e['pitch']))) for e in simultaneous]
positions = [generator.fretboard.TabPosition(by_id[e['id']]['tablature']['string'], by_id[e['id']]['tablature']['fret'], int(generator.model.fraction(e['pitch']))) for e in simultaneous]
self.assertEqual(len(positions), len({p.string_number for p in positions}))
self.assertTrue(generator.guitar.within_fret_span(positions))
def test_explicit_bass_and_default_root(self):
for symbol, expected in (('CM/E', 4), ('C7b9', 0)):
self.assertTrue(generator.fretboard.within_fret_span(positions))
def test_explicit_bass_is_preserved_for_triads_and_extended_chords(self):
for symbol, expected in (('CM/E', 4), ('C7b9/E', 4)):
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
chord = generator.harmony.parse_symbol(symbol)
source['staves'][0]['measures'][0]['harmonies'] = [{**chord, 'id': 'h1', 'onset': generator.model.rational(0)}]
@@ -88,9 +90,9 @@ class GuitarPartTests(unittest.TestCase):
self.assertEqual([generator.model.fraction(n['onset']) for n in notes if n['pitches']], [0, 2])
for onset in (0, 2):
pitches = [p for n in notes if generator.model.fraction(n['onset']) == onset for p in n['pitches']]
positions = [generator.guitar.TabPosition(p['tablature']['string'], p['tablature']['fret'], int(generator.model.pitch_number(p) - 12)) for p in pitches]
self.assertIn(tuple(sorted((p.string_number for p in positions), reverse=True)), generator.guitar.ACOUSTIC_STRINGS)
self.assertTrue(generator.guitar.within_fret_span(positions))
positions = [generator.fretboard.TabPosition(p['tablature']['string'], p['tablature']['fret'], int(generator.model.pitch_number(p) - 12)) for p in pitches]
self.assertIn(tuple(sorted((p.string_number for p in positions), reverse=True)), ((5, 4, 3, 2), (6, 4, 3, 2)))
self.assertTrue(generator.fretboard.within_fret_span(positions))
def test_long_melody_chords_and_short_notes(self):
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('E', 2, 'half', octave=4) + tests.test_musicxml.xml_note('G', 1, 'quarter', octave=4) + tests.test_musicxml.xml_note('C', 1, 'quarter', octave=5)
notes = tests.test_musicxml.notes(self.section(content))