From 768379305285265562834be5ea6aadbe4cd8c50f Mon Sep 17 00:00:00 2001 From: khodowany Date: Wed, 30 Sep 2026 08:05:28 -0500 Subject: [PATCH] Update Section Names --- TECHNICAL_GUIDE.md | 2 +- data/expected/performance_summary.json | 48 +++++++++++++------------- generator/arrange/form.py | 4 +-- tests/test_music_model.py | 2 +- tests/test_parts.py | 2 +- tests/test_scales.py | 10 ++++-- 6 files changed, 37 insertions(+), 31 deletions(-) diff --git a/TECHNICAL_GUIDE.md b/TECHNICAL_GUIDE.md index 9eaf6e5..0619c35 100644 --- a/TECHNICAL_GUIDE.md +++ b/TECHNICAL_GUIDE.md @@ -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. -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. diff --git a/data/expected/performance_summary.json b/data/expected/performance_summary.json index b05788a..c9d9cee 100644 --- a/data/expected/performance_summary.json +++ b/data/expected/performance_summary.json @@ -20,11 +20,11 @@ }, { "measure": 73, - "label": "Guitar Solo" + "label": "Acoustic Solo" }, { "measure": 105, - "label": "Guitar Solo" + "label": "Electric Solo" }, { "measure": 137, @@ -871,7 +871,7 @@ }, { "measure": 73, - "section_label": "Guitar Solo", + "section_label": "Acoustic Solo", "harmonies": [ { "onset": "0", @@ -1238,7 +1238,7 @@ }, { "measure": 105, - "section_label": "Guitar Solo", + "section_label": "Electric Solo", "harmonies": [ { "onset": "0", @@ -2092,11 +2092,11 @@ }, { "measure": 73, - "label": "Guitar Solo" + "label": "Acoustic Solo" }, { "measure": 105, - "label": "Guitar Solo" + "label": "Electric Solo" }, { "measure": 137, @@ -2943,7 +2943,7 @@ }, { "measure": 73, - "section_label": "Guitar Solo", + "section_label": "Acoustic Solo", "harmonies": [ { "onset": "0", @@ -3310,7 +3310,7 @@ }, { "measure": 105, - "section_label": "Guitar Solo", + "section_label": "Electric Solo", "harmonies": [ { "onset": "0", @@ -4164,11 +4164,11 @@ }, { "measure": 73, - "label": "Guitar Solo" + "label": "Acoustic Solo" }, { "measure": 105, - "label": "Guitar Solo" + "label": "Electric Solo" }, { "measure": 137, @@ -5283,7 +5283,7 @@ }, { "measure": 73, - "section_label": "Guitar Solo", + "section_label": "Acoustic Solo", "harmonies": [ { "onset": "0", @@ -5746,7 +5746,7 @@ }, { "measure": 105, - "section_label": "Guitar Solo", + "section_label": "Electric Solo", "harmonies": [ { "onset": "0", @@ -6877,11 +6877,11 @@ }, { "measure": 73, - "label": "Guitar Solo" + "label": "Acoustic Solo" }, { "measure": 105, - "label": "Guitar Solo" + "label": "Electric Solo" }, { "measure": 137, @@ -7823,7 +7823,7 @@ }, { "measure": 73, - "section_label": "Guitar Solo", + "section_label": "Acoustic Solo", "harmonies": [ { "onset": "0", @@ -8235,7 +8235,7 @@ }, { "measure": 105, - "section_label": "Guitar Solo", + "section_label": "Electric Solo", "harmonies": [ { "onset": "0", @@ -9175,11 +9175,11 @@ }, { "measure": 73, - "label": "Guitar Solo" + "label": "Acoustic Solo" }, { "measure": 105, - "label": "Guitar Solo" + "label": "Electric Solo" }, { "measure": 137, @@ -10121,7 +10121,7 @@ }, { "measure": 73, - "section_label": "Guitar Solo", + "section_label": "Acoustic Solo", "harmonies": [ { "onset": "0", @@ -10533,7 +10533,7 @@ }, { "measure": 105, - "section_label": "Guitar Solo", + "section_label": "Electric Solo", "harmonies": [ { "onset": "0", @@ -11473,11 +11473,11 @@ }, { "measure": 73, - "label": "Guitar Solo" + "label": "Acoustic Solo" }, { "measure": 105, - "label": "Guitar Solo" + "label": "Electric Solo" }, { "measure": 137, @@ -12687,7 +12687,7 @@ }, { "measure": 73, - "section_label": "Guitar Solo", + "section_label": "Acoustic Solo", "harmonies": [ { "onset": "0", @@ -13195,7 +13195,7 @@ }, { "measure": 105, - "section_label": "Guitar Solo", + "section_label": "Electric Solo", "harmonies": [ { "onset": "0", diff --git a/generator/arrange/form.py b/generator/arrange/form.py index a75f683..32463ae 100644 --- a/generator/arrange/form.py +++ b/generator/arrange/form.py @@ -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), ("Vocal Melody", body_start, body_measure_count, 1), ("Guitar Melody", body_start, body_measure_count, 2), - ("Guitar Solo", body_start, body_measure_count, 3), - ("Guitar Solo", body_start, body_measure_count, 4), + ("Acoustic Solo", body_start, body_measure_count, 3), + ("Electric Solo", body_start, body_measure_count, 4), ("Vocal Melody", body_start, body_measure_count, 5), ("Ending", edge_source_measure, edge_measure_count, 6), ) diff --git a/tests/test_music_model.py b/tests/test_music_model.py index 9a1d55a..60254b5 100644 --- a/tests/test_music_model.py +++ b/tests/test_music_model.py @@ -86,7 +86,7 @@ class MusicModelTests(unittest.TestCase): result = generator.arrange.generate.arrange_document(source, part, rhythms=tests.rhythm_fixtures.library()) generator.model.validate_document(result) 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.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'])) diff --git a/tests/test_parts.py b/tests/test_parts.py index 0b13d6a..49199b8 100644 --- a/tests/test_parts.py +++ b/tests/test_parts.py @@ -38,7 +38,7 @@ class PartGenerationTests(unittest.TestCase): generator.model.validate_document(result) for measure in result['staves'][0]['measures']: 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 self.assert_complete_measure(measure) sounding = [n for n in events if n['pitches']] diff --git a/tests/test_scales.py b/tests/test_scales.py index 420f650..2a5e0db 100644 --- a/tests/test_scales.py +++ b/tests/test_scales.py @@ -8,6 +8,7 @@ import shutil import tempfile import unittest +import generator.arrange.form import generator.arrange.generate import generator.engraving.cache import generator.engraving.fonts @@ -152,7 +153,8 @@ class ScaleOutputTests(unittest.TestCase): before = copy.deepcopy(source) warnings = io.StringIO() 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('No exact match within the permitted catalog', 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): source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([tests.test_musicxml.xml_note()] * 40)) 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)] fonts = generator.engraving.fonts.Fonts(pathlib.Path('package/assets/fonts/petaluma')) 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.assertTrue(all(len(p.systems) == 2 for p in layout.pages[:-1])) 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: 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) @@ -181,9 +184,12 @@ class ScaleOutputTests(unittest.TestCase): def test_only_soloing_part_is_annotated_without_revoicing(self): content = 'Cmajor' + tests.test_musicxml.xml_note() 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'): 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()) + 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', []) self.assertEqual(result, before) self.assertEqual(len(spans), int(part != 'vocal'))