Update Section Names
ci / checks (push) Successful in 47m26s

This commit is contained in:
khodowany
2026-09-30 08:05:28 -05:00
parent f5f0ab5eec
commit 7683793052
6 changed files with 37 additions and 31 deletions
+1 -1
View File
@@ -127,7 +127,7 @@ Measure durations, event onsets/durations, harmony offsets, span times, and pitc
Generated accompaniment has no ties. Acoustic attacks release at the earliest of their planned end, the next combined bass/upper attack, or the barline. Simultaneous bass/upper attacks use the shorter planned duration for the entire grip. If a binary duration would need ties, it shortens to the longest conventional single note that fits, followed by silence; no extra attack or artificial tuplet is introduced. Rhythm-snippet continuations are silent, and the planner does not count them as acoustic coverage. After placement, consolidation preserves the prepared timing and TAB assignments, combines simultaneous tones under one stem, and writes rests only for complete silence. A silent full bar has one measure rest. Old lane stem directions and beam groups are removed in favor of single-voice engraving. Authored melody ties, solo slashes, and electric melody harmonization remain separate from this build-time accompaniment policy. Generated accompaniment has no ties. Acoustic attacks release at the earliest of their planned end, the next combined bass/upper attack, or the barline. Simultaneous bass/upper attacks use the shorter planned duration for the entire grip. If a binary duration would need ties, it shortens to the longest conventional single note that fits, followed by silence; no extra attack or artificial tuplet is introduced. Rhythm-snippet continuations are silent, and the planner does not count them as acoustic coverage. After placement, consolidation preserves the prepared timing and TAB assignments, combines simultaneous tones under one stem, and writes rests only for complete silence. A silent full bar has one measure rest. Old lane stem directions and beam groups are removed in favor of single-voice engraving. Authored melody ties, solo slashes, and electric melody harmonization remain separate from this build-time accompaniment policy.
Acoustic accompaniment uses the planned foundation. Electric accompaniment sections in songs use the response plan; song electric melody sections are raised one octave before harmonization. Song sections remain Rhythm Lead-in, Vocal Melody, Guitar Melody, Guitar Solo, Guitar Solo, Vocal Melody, and Ending, with each part retaining its melody, accompaniment, rest, or solo-slash role. Optional turnarounds are removed before planning the final ending. The generated phrase cycles contain explicit events in reading order, not runtime loops or repeat signs. Acoustic accompaniment uses the planned foundation. Electric accompaniment sections in songs use the response plan; song electric melody sections are raised one octave before harmonization. Song sections remain Rhythm Lead-in, Vocal Melody, Guitar Melody, Acoustic Solo, Electric Solo, Vocal Melody, and Ending, with each part retaining its melody, accompaniment, rest, or solo-slash role. Optional turnarounds are removed before planning the final ending. The generated phrase cycles contain explicit events in reading order, not runtime loops or repeat signs.
Each source is parsed, validated, and normalized once for all three parts. The batch generator computes shared accompaniment plans before building those parts. Public `arrange_document` and `generate_performance_scores` calls require a keyword `rhythms` argument holding a `generator.rhythm.Library`; internal part builders receive precomputed plans. Builders treat the prepared source and plans as read-only and own their generated notes and spans. Source-path checks reject colliding song slugs before writing parts: for example, `one-two.xml` and `one_two.xml` would both produce `one-two-acoustic.json` and cannot coexist. Each source is parsed, validated, and normalized once for all three parts. The batch generator computes shared accompaniment plans before building those parts. Public `arrange_document` and `generate_performance_scores` calls require a keyword `rhythms` argument holding a `generator.rhythm.Library`; internal part builders receive precomputed plans. Builders treat the prepared source and plans as read-only and own their generated notes and spans. Source-path checks reject colliding song slugs before writing parts: for example, `one-two.xml` and `one_two.xml` would both produce `one-two-acoustic.json` and cannot coexist.
+24 -24
View File
@@ -20,11 +20,11 @@
}, },
{ {
"measure": 73, "measure": 73,
"label": "Guitar Solo" "label": "Acoustic Solo"
}, },
{ {
"measure": 105, "measure": 105,
"label": "Guitar Solo" "label": "Electric Solo"
}, },
{ {
"measure": 137, "measure": 137,
@@ -871,7 +871,7 @@
}, },
{ {
"measure": 73, "measure": 73,
"section_label": "Guitar Solo", "section_label": "Acoustic Solo",
"harmonies": [ "harmonies": [
{ {
"onset": "0", "onset": "0",
@@ -1238,7 +1238,7 @@
}, },
{ {
"measure": 105, "measure": 105,
"section_label": "Guitar Solo", "section_label": "Electric Solo",
"harmonies": [ "harmonies": [
{ {
"onset": "0", "onset": "0",
@@ -2092,11 +2092,11 @@
}, },
{ {
"measure": 73, "measure": 73,
"label": "Guitar Solo" "label": "Acoustic Solo"
}, },
{ {
"measure": 105, "measure": 105,
"label": "Guitar Solo" "label": "Electric Solo"
}, },
{ {
"measure": 137, "measure": 137,
@@ -2943,7 +2943,7 @@
}, },
{ {
"measure": 73, "measure": 73,
"section_label": "Guitar Solo", "section_label": "Acoustic Solo",
"harmonies": [ "harmonies": [
{ {
"onset": "0", "onset": "0",
@@ -3310,7 +3310,7 @@
}, },
{ {
"measure": 105, "measure": 105,
"section_label": "Guitar Solo", "section_label": "Electric Solo",
"harmonies": [ "harmonies": [
{ {
"onset": "0", "onset": "0",
@@ -4164,11 +4164,11 @@
}, },
{ {
"measure": 73, "measure": 73,
"label": "Guitar Solo" "label": "Acoustic Solo"
}, },
{ {
"measure": 105, "measure": 105,
"label": "Guitar Solo" "label": "Electric Solo"
}, },
{ {
"measure": 137, "measure": 137,
@@ -5283,7 +5283,7 @@
}, },
{ {
"measure": 73, "measure": 73,
"section_label": "Guitar Solo", "section_label": "Acoustic Solo",
"harmonies": [ "harmonies": [
{ {
"onset": "0", "onset": "0",
@@ -5746,7 +5746,7 @@
}, },
{ {
"measure": 105, "measure": 105,
"section_label": "Guitar Solo", "section_label": "Electric Solo",
"harmonies": [ "harmonies": [
{ {
"onset": "0", "onset": "0",
@@ -6877,11 +6877,11 @@
}, },
{ {
"measure": 73, "measure": 73,
"label": "Guitar Solo" "label": "Acoustic Solo"
}, },
{ {
"measure": 105, "measure": 105,
"label": "Guitar Solo" "label": "Electric Solo"
}, },
{ {
"measure": 137, "measure": 137,
@@ -7823,7 +7823,7 @@
}, },
{ {
"measure": 73, "measure": 73,
"section_label": "Guitar Solo", "section_label": "Acoustic Solo",
"harmonies": [ "harmonies": [
{ {
"onset": "0", "onset": "0",
@@ -8235,7 +8235,7 @@
}, },
{ {
"measure": 105, "measure": 105,
"section_label": "Guitar Solo", "section_label": "Electric Solo",
"harmonies": [ "harmonies": [
{ {
"onset": "0", "onset": "0",
@@ -9175,11 +9175,11 @@
}, },
{ {
"measure": 73, "measure": 73,
"label": "Guitar Solo" "label": "Acoustic Solo"
}, },
{ {
"measure": 105, "measure": 105,
"label": "Guitar Solo" "label": "Electric Solo"
}, },
{ {
"measure": 137, "measure": 137,
@@ -10121,7 +10121,7 @@
}, },
{ {
"measure": 73, "measure": 73,
"section_label": "Guitar Solo", "section_label": "Acoustic Solo",
"harmonies": [ "harmonies": [
{ {
"onset": "0", "onset": "0",
@@ -10533,7 +10533,7 @@
}, },
{ {
"measure": 105, "measure": 105,
"section_label": "Guitar Solo", "section_label": "Electric Solo",
"harmonies": [ "harmonies": [
{ {
"onset": "0", "onset": "0",
@@ -11473,11 +11473,11 @@
}, },
{ {
"measure": 73, "measure": 73,
"label": "Guitar Solo" "label": "Acoustic Solo"
}, },
{ {
"measure": 105, "measure": 105,
"label": "Guitar Solo" "label": "Electric Solo"
}, },
{ {
"measure": 137, "measure": 137,
@@ -12687,7 +12687,7 @@
}, },
{ {
"measure": 73, "measure": 73,
"section_label": "Guitar Solo", "section_label": "Acoustic Solo",
"harmonies": [ "harmonies": [
{ {
"onset": "0", "onset": "0",
@@ -13195,7 +13195,7 @@
}, },
{ {
"measure": 105, "measure": 105,
"section_label": "Guitar Solo", "section_label": "Electric Solo",
"harmonies": [ "harmonies": [
{ {
"onset": "0", "onset": "0",
+2 -2
View File
@@ -46,8 +46,8 @@ def full_arrangement_sections(measures: list[dict], part_type: str) -> list[Arra
("Rhythm Lead-in", edge_source_measure, edge_measure_count, 0), ("Rhythm Lead-in", edge_source_measure, edge_measure_count, 0),
("Vocal Melody", body_start, body_measure_count, 1), ("Vocal Melody", body_start, body_measure_count, 1),
("Guitar Melody", body_start, body_measure_count, 2), ("Guitar Melody", body_start, body_measure_count, 2),
("Guitar Solo", body_start, body_measure_count, 3), ("Acoustic Solo", body_start, body_measure_count, 3),
("Guitar Solo", body_start, body_measure_count, 4), ("Electric Solo", body_start, body_measure_count, 4),
("Vocal Melody", body_start, body_measure_count, 5), ("Vocal Melody", body_start, body_measure_count, 5),
("Ending", edge_source_measure, edge_measure_count, 6), ("Ending", edge_source_measure, edge_measure_count, 6),
) )
+1 -1
View File
@@ -86,7 +86,7 @@ class MusicModelTests(unittest.TestCase):
result = generator.arrange.generate.arrange_document(source, part, rhythms=tests.rhythm_fixtures.library()) result = generator.arrange.generate.arrange_document(source, part, rhythms=tests.rhythm_fixtures.library())
generator.model.validate_document(result) generator.model.validate_document(result)
measures = result['staves'][0]['measures'] measures = result['staves'][0]['measures']
self.assertEqual(sum(bool(m.get('section_label')) for m in measures), 7) self.assertEqual([m['section_label'] for m in measures if m.get('section_label')], ['Rhythm Lead-in', 'Vocal Melody', 'Guitar Melody', 'Acoustic Solo', 'Electric Solo', 'Vocal Melody', 'Ending'])
self.assertFalse(measures[-1]['harmonies']) self.assertFalse(measures[-1]['harmonies'])
self.assertTrue(any(h for m in measures[:-1] for h in m['harmonies'])) self.assertTrue(any(h for m in measures[:-1] for h in m['harmonies']))
self.assertFalse(any('chord_optional' in h for m in measures for h in m['harmonies'])) self.assertFalse(any('chord_optional' in h for m in measures for h in m['harmonies']))
+1 -1
View File
@@ -38,7 +38,7 @@ class PartGenerationTests(unittest.TestCase):
generator.model.validate_document(result) generator.model.validate_document(result)
for measure in result['staves'][0]['measures']: for measure in result['staves'][0]['measures']:
events = measure['voices'][0]['notes'] events = measure['voices'][0]['notes']
if measure.get('section_label') in ('Rhythm Lead-in', 'Guitar Melody', 'Guitar Solo'): if measure.get('section_label') in ('Rhythm Lead-in', 'Guitar Melody', 'Acoustic Solo', 'Electric Solo'):
continue continue
self.assert_complete_measure(measure) self.assert_complete_measure(measure)
sounding = [n for n in events if n['pitches']] sounding = [n for n in events if n['pitches']]
+8 -2
View File
@@ -8,6 +8,7 @@ import shutil
import tempfile import tempfile
import unittest import unittest
import generator.arrange.form
import generator.arrange.generate import generator.arrange.generate
import generator.engraving.cache import generator.engraving.cache
import generator.engraving.fonts import generator.engraving.fonts
@@ -152,7 +153,8 @@ class ScaleOutputTests(unittest.TestCase):
before = copy.deepcopy(source) before = copy.deepcopy(source)
warnings = io.StringIO() warnings = io.StringIO()
with contextlib.redirect_stdout(warnings): with contextlib.redirect_stdout(warnings):
report = generator.scales.mapping.annotate(source, [generator.scales.timeline.Solo(0, 12, 'Guitar Solo')], tests.scale_fixtures.library(), 'fixture.xml') report = generator.scales.mapping.annotate(source, [generator.scales.timeline.Solo(0, 12, 'Acoustic Solo')], tests.scale_fixtures.library(), 'fixture.xml')
self.assertTrue(report.startswith('Acoustic: Acoustic Solo\n'))
self.assertIn('fixture.xml, Acoustic, source measure 3', warnings.getvalue()) self.assertIn('fixture.xml, Acoustic, source measure 3', warnings.getvalue())
self.assertIn('No exact match within the permitted catalog', report) self.assertIn('No exact match within the permitted catalog', report)
self.assertIn('Parent degree: 1; chord-root mode: C Ionian', report) self.assertIn('Parent degree: 1; chord-root mode: C Ionian', report)
@@ -166,7 +168,7 @@ class ScaleOutputTests(unittest.TestCase):
def test_layout_keeps_exact_offsets_continuations_and_uniform_size(self): def test_layout_keeps_exact_offsets_continuations_and_uniform_size(self):
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([tests.test_musicxml.xml_note()] * 40)) source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([tests.test_musicxml.xml_note()] * 40))
staff = source['staves'][0] staff = source['staves'][0]
staff['measures'][0]['section_label'] = 'Guitar Solo' staff['measures'][0]['section_label'] = 'Electric Solo'
staff['scale_spans'] = [{'id': f'scale-{i}', 'start': generator.model.rational(a), 'end': generator.model.rational(b), 'label': label, 'matched': True} for i, (a, b, label) in enumerate(((0, fractions.Fraction(3, 2), 'C Ionian'), (fractions.Fraction(3, 2), 60, 'D Harmonic Minor')), 1)] staff['scale_spans'] = [{'id': f'scale-{i}', 'start': generator.model.rational(a), 'end': generator.model.rational(b), 'label': label, 'matched': True} for i, (a, b, label) in enumerate(((0, fractions.Fraction(3, 2), 'C Ionian'), (fractions.Fraction(3, 2), 60, 'D Harmonic Minor')), 1)]
fonts = generator.engraving.fonts.Fonts(pathlib.Path('package/assets/fonts/petaluma')) fonts = generator.engraving.fonts.Fonts(pathlib.Path('package/assets/fonts/petaluma'))
layout = generator.engraving.layout.layout_score(source, generator.engraving.types.Style(), fonts) layout = generator.engraving.layout.layout_score(source, generator.engraving.types.Style(), fonts)
@@ -174,6 +176,7 @@ class ScaleOutputTests(unittest.TestCase):
self.assertEqual(len({s.scale for s in systems}), 1) self.assertEqual(len({s.scale for s in systems}), 1)
self.assertTrue(all(len(p.systems) == 2 for p in layout.pages[:-1])) self.assertTrue(all(len(p.systems) == 2 for p in layout.pages[:-1]))
self.assertGreater(len(systems), 2) self.assertGreater(len(systems), 2)
self.assertEqual([p.value for s in systems for p in s.primitives if p.role == 'section'], ['Electric Solo'])
for system in systems: for system in systems:
expected = [s['label'] for s in staff['scale_spans'] if generator.model.fraction(s['start']) < system.end and generator.model.fraction(s['end']) > system.start] expected = [s['label'] for s in staff['scale_spans'] if generator.model.fraction(s['start']) < system.end and generator.model.fraction(s['end']) > system.start]
self.assertEqual([p.value for p in system.primitives if p.role == 'scale'], expected) self.assertEqual([p.value for p in system.primitives if p.role == 'scale'], expected)
@@ -181,9 +184,12 @@ class ScaleOutputTests(unittest.TestCase):
def test_only_soloing_part_is_annotated_without_revoicing(self): def test_only_soloing_part_is_annotated_without_revoicing(self):
content = '<harmony><root><root-step>C</root-step></root><kind>major</kind></harmony>' + tests.test_musicxml.xml_note() content = '<harmony><root><root-step>C</root-step></root><kind>major</kind></harmony>' + tests.test_musicxml.xml_note()
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content)) source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content))
roles = {'acoustic': ('slash', 'accompaniment'), 'electric': ('rest', 'slash'), 'vocal': ('rest', 'rest')}
for part in ('acoustic', 'electric', 'vocal'): for part in ('acoustic', 'electric', 'vocal'):
before = generator.arrange.generate.arrange_document(source, part, rhythms=tests.rhythm_fixtures.library()) before = generator.arrange.generate.arrange_document(source, part, rhythms=tests.rhythm_fixtures.library())
result = generator.arrange.generate.arrange_document(source, part, rhythms=tests.rhythm_fixtures.library(), scales=tests.scale_fixtures.library()) result = generator.arrange.generate.arrange_document(source, part, rhythms=tests.rhythm_fixtures.library(), scales=tests.scale_fixtures.library())
sections = generator.arrange.form.full_arrangement_sections(source['staves'][0]['measures'], part)
self.assertEqual([(s.label, s.role) for s in sections[3:5]], list(zip(('Acoustic Solo', 'Electric Solo'), roles[part])))
spans = result['staves'][0].pop('scale_spans', []) spans = result['staves'][0].pop('scale_spans', [])
self.assertEqual(result, before) self.assertEqual(result, before)
self.assertEqual(len(spans), int(part != 'vocal')) self.assertEqual(len(spans), int(part != 'vocal'))