Toggle Solos
ci / checks (push) Failing after 24m18s

This commit is contained in:
khodowany
2026-10-08 22:11:34 -05:00
parent 1bacfd0f33
commit 150394d598
31 changed files with 8707 additions and 372 deletions
+109
View File
@@ -0,0 +1,109 @@
"""Improvisation replaces source-allocated solos, including their pickups."""
import copy
import fractions
import pathlib
import unittest
import generator.arrange.assembly
import generator.arrange.form
import generator.arrange.generate
import generator.composition
import generator.composition_types
import generator.engraving.fonts
import generator.engraving.layout
import generator.engraving.types
import generator.model
import generator.musicxml
import generator.parts
import generator.performance
import generator.source
import generator.validation
import tests.production_fixtures
import tests.solo_fixtures
import tests.test_arrangement_pickup
import tests.test_musicxml
F = fractions.Fraction
def events(document):
cursor = F()
result = []
for measure in document['staves'][0]['measures']:
for voice in measure['voices']:
for note in voice['notes']:
start = cursor + generator.model.fraction(note['onset'])
result.append((start, start + generator.model.fraction(note['duration']), note))
cursor += generator.model.fraction(measure['duration'])
return result
class ImprovisationTests(unittest.TestCase):
def setUp(self):
tests.production_fixtures.install(self)
def check_variants(self, document):
source = generator.source.SourceAnalysis.read(generator.parts.prepare_document(document))
results = {r: generator.composition.generate_role(source, r, pathlib.Path('data')) for r in generator.composition_types.ROLES}
before = copy.deepcopy(results)
written = generator.arrange.assembly.assemble(source, results)
improv = generator.arrange.assembly.assemble(source, results, solos_enabled=False)
self.assertEqual(results, before)
self.assertEqual(written['vocal'], improv['vocal'])
for part in ('acoustic', 'electric'):
windows = [(a, b) for a, b, role in generator.arrange.form.windows(source.document(), part) if role == part + '_solo']
actual = events(improv[part])
slashes = [(a, b) for a, b, n in actual if n.get('notehead') == 'slash']
self.assertTrue(slashes)
for start, end in windows:
cursor = start
for a, b in slashes:
if start <= a < end:
self.assertEqual(a, cursor)
self.assertLessEqual(b, end)
cursor = b
self.assertEqual(cursor, end)
self.assertFalse([n for a, b, n in actual if n['pitches'] and any(a < end and b > start for start, end in windows)])
outside = lambda doc: [(a, b, n) for a, b, n in events(doc) if not any(a < end and b > start for start, end in windows)]
self.assertEqual(outside(written[part]), outside(improv[part]))
for a, b in zip(written[part]['staves'][0]['measures'], improv[part]['staves'][0]['measures']):
for key in ('harmonies', 'section_label', 'number', 'duration', 'attributes'):
self.assertEqual(a.get(key), b.get(key), key)
self.assertEqual(generator.performance.timing(written[part]), generator.performance.timing(improv[part]))
generator.validation.validate_document(improv[part])
return improv
def test_no_pickup_and_simple_meters(self):
for meter in (3, 4, 5):
with self.subTest(meter=meter):
self.check_variants(tests.solo_fixtures.song(count=2, meter=meter, chords=('CM7',)))
def test_partial_pickup_includes_allocated_rests_and_preserves_earlier_music(self):
pickup = '<note><rest/><duration>1</duration><type>eighth</type></note>' + tests.test_musicxml.xml_note('D', 1, 'eighth')
self.check_variants(tests.test_arrangement_pickup.source(pickup, divisions=2))
def test_repertoire_pickup_allocations(self):
for song, first in (('all-of-me', (288, 416)), ('autumn_leaves', (284, 412))):
with self.subTest(song=song):
variants = self.check_variants(generator.musicxml.load_musicxml(pathlib.Path('data/songs') / (song + '.xml')))
for part, expected in zip(('acoustic', 'electric'), first):
slashes = [(a, b) for a, b, n in events(variants[part]) if n.get('notehead') == 'slash']
self.assertEqual(slashes[0][0], expected)
self.assertTrue(all(b - a == 1 for a, b in slashes))
def test_meter_changes_and_compound_beats_render_in_staff_and_tab(self):
change = '<attributes><time><beats>6</beats><beat-type>8</beat-type></time></attributes>'
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(duration=1, kind='quarter') + change + tests.test_musicxml.xml_note(duration=3, kind='half', extra='<dot/>')))
source = generator.source.SourceAnalysis.read(generator.parts.prepare_document(document))
template = generator.arrange.generate._arrange_document(source.document(), 'acoustic', owned={'slash'})
template['staves'][0]['tablature'] = True
result = generator.arrange.assembly.improvisation(source, template, 'acoustic')
result['metadata']['solos_enabled'] = False
solo = next(m for m in result['staves'][0]['measures'] if m.get('section_label') == 'Acoustic Solo')
self.assertEqual([generator.model.fraction(n['duration']) for n in solo['voices'][0]['notes']], [1, F(3, 2), F(3, 2)])
generator.validation.validate_document(result)
fonts = generator.engraving.fonts.Fonts(pathlib.Path('package/assets/fonts/petaluma'))
layout = generator.engraving.layout.layout_score(result, generator.engraving.types.Style(), fonts)
primitives = [p for page in layout.pages for p in page.primitives]
self.assertEqual(sum(p.role == 'slash' for p in primitives), 3)
self.assertEqual(sum(p.role == 'tab_slash' for p in primitives), 3)
self.assertFalse(any(p.role in ('tab', 'head') for p in primitives))
self.assertTrue(any(p.role == 'subtitle' and 'Improvisation' in p.value for p in primitives))