Fix Generation
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import copy
|
||||
import fractions
|
||||
import pathlib
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
import generator.arrange.form
|
||||
import generator.arrange.generate
|
||||
import generator.electric
|
||||
import generator.guitar
|
||||
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.guitar.STANDARD_TUNING)
|
||||
positions = tuple(generator.guitar.TabPosition(string, fret, tuning[string] + fret) for string, fret in zip((4, 3, 2, 1), frets))
|
||||
pitches = generator.guitar.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_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'})
|
||||
Reference in New Issue
Block a user