Files
product-vamp/tests/test_electric_voicing.py
T
khodowany 09656a9d60
ci / checks (push) Successful in 48m34s
Package
2026-09-30 19:46:37 -05:00

138 lines
8.4 KiB
Python

import copy
import fractions
import pathlib
import unittest
import unittest.mock
import generator.arrange.form
import generator.arrange.generate
import generator.electric
import generator.fretboard
import generator.harmony
import generator.model
import generator.musicxml
import tests.rhythm_fixtures
def harmony(symbol, onset=0):
return generator.harmony.parse_symbol(symbol) | {'id': f'chord-{onset}', 'onset': generator.model.rational(onset)}
def note(identifier, onset, chord, frets, *, melody=False, duration=1):
tuning = dict(generator.fretboard.STANDARD_TUNING)
positions = tuple(generator.fretboard.TabPosition(string, fret, tuning[string] + fret) for string, fret in zip((4, 3, 2, 1), frets))
pitches = generator.fretboard.chord_pitches(chord, positions)
result = generator.model.make_event(identifier, fractions.Fraction(onset), fractions.Fraction(duration), 'note', pitches)
result.update(_harmony=chord, _guitar_profile='electric')
if melody:
result['_harmonize'] = True
return result
def measure(notes, harmonies=()):
return {'number': '1', 'duration': generator.model.rational(4), 'harmonies': list(harmonies), 'voices': [{'id': 'voice', 'notes': notes}]}
def shape(event):
return tuple(sorted((p['tablature']['string'], p['tablature']['fret']) for p in event['pitches']))
class ElectricVoicingTests(unittest.TestCase):
def test_mixed_pickup_bar_links_each_role_to_its_original_passage(self):
chord = harmony('C7')
pickup = note('pickup', 3, chord, (2, 3, 1, 3), melody=True)
outgoing = note('outgoing', 0, chord, (5, 3, 5, 3))
target = note('target', 0, chord, (10, 9, 11, 8))
mixed = measure([outgoing, pickup])
source = [measure([], [chord]), measure([], [harmony('G7')]), measure([], [chord])]
origins = [generator.electric.MeasureOrigin(fractions.Fraction(8), 'accompaniment', mixed, end=fractions.Fraction(3)), generator.electric.MeasureOrigin(fractions.Fraction(-3), 'melody', mixed, start=fractions.Fraction(3)), generator.electric.MeasureOrigin(fractions.Fraction(0), 'accompaniment', measure([target]))]
links = generator.electric.link_voicings(source, origins)
self.assertEqual(len(links), 1)
self.assertIs(links[0].target, target)
self.assertEqual(links[0].references, (pickup,))
def test_corresponding_melody_shapes_change_within_one_harmony(self):
chord = harmony('C7')
first = note('melody-1', 1, chord, (2, 3, 1, 3), melody=True, duration=2)
second = note('melody-2', 2, chord, (10, 9, 11, 8), melody=True, duration=2)
earlier = [note(f'accompaniment-{i}', i, chord, (5, 3, 5, 3)) for i in (0, 2)]
later = [note(f'accompaniment-next-{i}', i, chord, (5, 3, 5, 3)) for i in (0, 3)]
source = [measure([], [chord]), measure([], [copy.deepcopy(chord)])]
origins = [generator.electric.MeasureOrigin(fractions.Fraction(0), 'melody', measure([first])), generator.electric.MeasureOrigin(fractions.Fraction(4), 'melody', measure([second])), generator.electric.MeasureOrigin(fractions.Fraction(0), 'accompaniment', measure(earlier)), generator.electric.MeasureOrigin(fractions.Fraction(4), 'accompaniment', measure(later))]
links = generator.electric.link_voicings(source, origins)
self.assertEqual(len(links), 4)
before = copy.deepcopy((first, second))
generator.electric.reuse_voicings(links)
self.assertEqual((first, second), before)
self.assertEqual([shape(n) for n in earlier + later], [shape(first)] * 3 + [shape(second)])
self.assertTrue(all(p['duration'] == n['duration'] and p['written'] == n['written'] for n in earlier + later for p in n['pitches']))
def test_links_follow_actual_placement_without_copying_melody_ties(self):
chord = harmony('FM7')
reference = note('melody', 0, chord, (3, 5, 5, 5), melody=True, duration=2)
target = note('stab', 1, chord, (3, 2, 1, 0))
origins = [generator.electric.MeasureOrigin(fractions.Fraction(), role, measure([n])) for role, n in (('melody', reference), ('accompaniment', target))]
links = generator.electric.link_voicings([measure([], [chord])], origins)
for pitch in reference['pitches']:
pitch['tie_to'] = 'next-' + pitch['id']
before = copy.deepcopy(reference)
generator.electric.reuse_voicings(links)
self.assertEqual(reference, before)
self.assertEqual(shape(reference), shape(target))
self.assertTrue(all('tie_to' not in p and 'tie_from' not in p for p in target['pitches']))
self.assertEqual({p['id'] for p in target['pitches']}, {f'stab_s{i}' for i in range(1, 5)})
target['pitches'][0]['tablature']['fret'] = 20
self.assertEqual(reference, before)
def test_unharmonized_passage_does_not_borrow_from_another_occurrence(self):
chord = harmony('C7')
reference = note('melody', 0, chord, (2, 3, 1, 3), melody=True)
target = note('stab', 0, chord, (5, 3, 5, 3))
origins = [generator.electric.MeasureOrigin(fractions.Fraction(), 'melody', measure([reference])), generator.electric.MeasureOrigin(fractions.Fraction(8), 'accompaniment', measure([target]))]
for middle in (harmony('G7'), {'kind': 'none', 'degrees': [], 'onset': generator.model.rational(0)}):
source = [measure([], [chord]), measure([], [middle]), measure([], [chord])]
self.assertEqual(generator.electric.link_voicings(source, origins), [])
def test_mid_measure_changes_and_omitted_turnaround_do_not_copy_wrong_chord(self):
first, second = harmony('C7'), harmony('FM7', 2)
source = [measure([], [first, second])]
reference = note('melody', 2, second, (3, 5, 5, 5), melody=True)
target = note('stab', 3, first, (2, 3, 1, 3))
origins = [generator.electric.MeasureOrigin(fractions.Fraction(), role, measure([n])) for role, n in (('melody', reference), ('accompaniment', target))]
self.assertEqual(generator.electric.link_voicings(source, origins), [])
target['_harmony'] = second
links = generator.electric.link_voicings(source, origins)
self.assertEqual(len(links), 1)
generator.electric.reuse_voicings(links)
self.assertEqual(shape(reference), shape(target))
def test_failed_melody_harmonization_keeps_independent_accompaniment(self):
chord = harmony('FM7')
reference = note('melody', 0, chord, (3, 5, 5, 5), melody=True)
target = note('stab', 1, chord, (3, 2, 1, 0))
origins = [generator.electric.MeasureOrigin(fractions.Fraction(), role, measure([n])) for role, n in (('melody', reference), ('accompaniment', target))]
links = generator.electric.link_voicings([measure([], [chord])], origins)
reference['pitches'] = reference['pitches'][:1]
before = copy.deepcopy(target)
generator.electric.reuse_voicings(links)
self.assertEqual(target, before)
def test_real_arrangement_reuses_requested_grips_without_changing_melody(self):
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/autumn_leaves.xml'))
before = copy.deepcopy(source)
rhythms = tests.rhythm_fixtures.library()
with unittest.mock.patch('generator.electric.reuse_voicings'):
independent = generator.arrange.generate.arrange_document(source, 'electric', rhythms=rhythms)
result = generator.arrange.generate.arrange_document(source, 'electric', rhythms=rhythms)
generator.model.validate_document(result)
self.assertEqual(source, before)
start, checked = 0, set()
for section in generator.arrange.form.full_arrangement_sections(source['staves'][0]['measures'], 'electric'):
measures = result['staves'][0]['measures'][start:start + section.measure_count]
original = independent['staves'][0]['measures'][start:start + section.measure_count]
start += section.measure_count
if section.role == 'melody':
self.assertEqual(measures, original)
if section.role != 'accompaniment':
continue
for m, old in zip(measures, original):
for voice, old_voice in zip(m['voices'], old['voices']):
self.assertEqual([{k: v for k, v in n.items() if k != 'pitches'} for n in voice['notes']], [{k: v for k, v in n.items() if k != 'pitches'} for n in old_voice['notes']])
for n in voice['notes']:
if not n['pitches'] or m['number'] not in ('4', '12', '22', '24'):
continue
expected = (3, 3, 3, 3) if m['number'] == '22' else (3, 5, 5, 5)
self.assertEqual(tuple(f for _, f in sorted(shape(n), reverse=True)), expected)
self.assertTrue(all('tie_from' not in p and 'tie_to' not in p for p in n['pitches']))
checked.add(m['number'])
self.assertEqual(checked, {'4', '12', '22', '24'})