Files
product-vamp/tests/test_arrangement_ending.py
T
khodowany c7f6a30a13 Comping
2026-10-06 21:28:42 -05:00

94 lines
4.7 KiB
Python

"""The held recording tail has exact score time and no vocal attacks."""
import copy
import fractions
import pathlib
import unittest
import generator.acoustic
import generator.arrange.ending
import generator.arrange.generate
import generator.fretboard
import generator.harmony
import generator.model
import generator.musicxml
import generator.parts
import generator.performance
import generator.production
import generator.score
import tests.solo_fixtures
class ArrangementEnding(unittest.TestCase):
def test_current_sources_end_on_effective_harmony_with_one_attack(self):
for name, expected in (('all-of-me', 'GM6'), ('autumn_leaves', 'Dm')):
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs') / (name + '.xml'))
before = copy.deepcopy(source)
for part in ('acoustic', 'electric', 'vocal'):
result = generator.arrange.generate.arrange_document(source, part)
measures = result['staves'][0]['measures']
tail = measures[-3:]
self.assertEqual(len(measures), 171)
self.assertEqual([m['number'] for m in tail], ['169', '170', '171'])
self.assertEqual(generator.harmony.symbol(tail[0]['harmonies'][0]), expected)
self.assertEqual([m['section_label'] for m in tail], ['Ending', '', ''])
self.assertTrue(all(m['ending_sustain'] for m in tail))
notes = [n for m in tail for n in generator.score.measure_notes(m)]
attacks = [n for n in notes if n['pitches'] and not any(p.get('tie_from') for p in n['pitches'])]
self.assertEqual(len(attacks), 0 if part == 'vocal' else 1)
if part == 'vocal':
self.assertTrue(all(n['kind'] == 'rest' and not n.get('lyrics') for n in notes))
else:
self.assertEqual(sum(generator.model.fraction(n['duration']) for n in notes if n['pitches']), 12)
self.assertEqual([p['tablature'] for p in notes[0]['pitches']], [p['tablature'] for p in notes[-1]['pitches']])
if part == 'acoustic':
self.assertIn(tuple(p['tablature']['string'] for p in attacks[0]['pitches']), ((5, 4, 3, 2), (6, 4, 3, 2)))
timing = generator.performance.timing(result)
self.assertEqual(generator.model.fraction(timing['sustain_start']), 672)
self.assertEqual(generator.model.fraction(timing['duration']), 684)
self.assertEqual(len(timing['clicks']), 672)
generator.model.validate_document(result)
generator.fretboard.validate_document(result)
self.assertEqual(source, before)
def test_ending_uses_final_meter_and_does_not_extend_solo_windows(self):
for meter in (3, 5):
source = tests.solo_fixtures.song(count=2, meter=meter, chords=('C7', 'FM7'))
prepared = generator.parts.prepare_document(source)
for part in ('acoustic', 'electric'):
role = part + '_accompaniment'
result = generator.production.generate_role(generator.production.SourceAnalysis.read(prepared), role, pathlib.Path('data'))
tail = result['document']['staves'][0]['measures'][-3:]
self.assertEqual([generator.model.fraction(m['duration']) for m in tail], [meter] * 3)
windows = generator.production.windows(prepared, part)
self.assertEqual(windows[-1][2], role)
self.assertEqual(windows[-1][1] - windows[-1][0], 3 * meter)
self.assertEqual(sum(b - a for a, b, r in windows if r == part + '_solo'), 2 * meter)
def test_missing_or_explicitly_absent_final_chord_is_not_invented(self):
for no_chord in (False, True):
source = tests.solo_fixtures.song(count=1, chords=('C7',))
last = source['staves'][0]['measures'][-1]
if no_chord:
last['harmonies'][0]['kind'] = 'none'
else:
last['harmonies'] = []
with self.assertRaisesRegex(ValueError, 'final performed harmony'):
generator.arrange.generate.arrange_document(source, 'acoustic')
def test_partial_combination_and_short_overlap_fail_after_tie_merging(self):
source = tests.solo_fixtures.song(count=1, chords=('C7',))
doc = generator.arrange.generate.arrange_document(source, 'acoustic')
tail = copy.deepcopy(doc)
tail['staves'][0]['measures'] = tail['staves'][0]['measures'][-3:]
generator.acoustic.validate_comping(tail)
first = generator.score.measure_notes(tail['staves'][0]['measures'][0])[0]
bass = first['pitches'][0]
bass.pop('tie_to')
bass['duration'] = generator.model.rational(1)
with self.assertRaises(ValueError):
generator.acoustic.validate_comping(tail)
def test_ending_markers_cannot_interrupt_the_main_score(self):
source = tests.solo_fixtures.song(count=3, chords=('C7',))
source['staves'][0]['measures'][0]['ending_sustain'] = True
with self.assertRaisesRegex(ValueError, 'contiguous'):
generator.performance.timing(source)
if __name__ == '__main__':
unittest.main()